Skip to content

Instantly share code, notes, and snippets.

@tom2strobl
Last active November 4, 2021 11:38
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tom2strobl/553eee17c2ec72ad04fa20bca97860c3 to your computer and use it in GitHub Desktop.
Save tom2strobl/553eee17c2ec72ad04fa20bca97860c3 to your computer and use it in GitHub Desktop.
basically the pirsch-sdk client, without getters and fetch instead of axios to make it run on an edge / service worker environment
import { IncomingMessage } from 'http'
import { NextRequest } from 'next/server'
import { ClientConfig, AuthenticationResponse, APIError, Hit } from 'pirsch-sdk/types'
const referrerQueryParams = ['ref', 'referer', 'referrer', 'source', 'utm_source']
const defaultBaseURL = 'https://api.pirsch.io'
const defaultTimeout = 5000
const defaultProtocol = 'http'
const authenticationEndpoint = '/api/v1/token'
const hitEndpoint = '/api/v1/hit'
const eventEndpoint = '/api/v1/event'
const createFetchClient = ({ baseURL }: { baseURL: string }) => {
return {
post: async function (url = '', data = {}, options = {}) {
return fetch(baseURL + url, {
method: 'POST', // *GET, POST, PUT, DELETE, etc.
body: JSON.stringify(data), // body data type must match "Content-Type" header
...options // Additional request options
})
}
}
}
type FetchClient = ReturnType<typeof createFetchClient>
/**
* Client is used to access the Pirsch API.
*/
export class Client {
private readonly clientID: string
private readonly clientSecret: string
private readonly hostname: string
private readonly protocol: string
private client: FetchClient
private accessToken: string = ''
/**
* The constructor creates a new client.
*
* @param config You need to pass in the hostname, client ID, and client secret you have configured on the Pirsch dashboard.
* It's also recommended to set the propper protocol for your website, else it will be set to http by default.
* All other configuration parameters can be left to their defaults.
*/
constructor(config: ClientConfig) {
if (!config.baseURL) {
config.baseURL = defaultBaseURL
}
if (!config.timeout) {
config.timeout = defaultTimeout
}
if (!config.protocol) {
config.protocol = defaultProtocol
}
this.clientID = config.clientID
this.clientSecret = config.clientSecret
this.hostname = config.hostname
this.protocol = config.protocol
this.client = createFetchClient({
baseURL: config.baseURL
})
}
/**
* hit sends a hit to Pirsch. Make sure you call it in all request handlers you want to track.
* Also, make sure to filter out unwanted pathnames (like /favicon.ico in your root handler for example).
*
* @param hit all required data for the request.
* @param retry retry the request in case a 401 (unauthenticated) error is returned. Don't modify this.
* @returns APIError or an empty promise, in case something went wrong
*/
async hit(hit: Hit, retry: boolean = true): Promise<APIError | null> {
try {
if (hit.dnt === '1') {
return Promise.resolve<null>(null)
}
const postAction = await this.client.post(
hitEndpoint,
{
hostname: this.hostname,
...hit
},
{
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${this.accessToken}`
}
}
)
if (!postAction.ok && retry) {
try {
await this.refreshToken()
return this.hit(hit, false)
} catch (e) {
return e as APIError
}
}
return Promise.resolve<null>(null)
} catch (e: any) {
return Promise.reject(e)
}
}
/**
* event sends an event to Pirsch. Make sure you call it in all request handlers you want to track.
* Also, make sure to filter out unwanted pathnames (like /favicon.ico in your root handler for example).
*
* @param name the name for the event
* @param hit all required data for the request
* @param duration optional duration for the event
* @param meta optional object containing metadata (only scalar values, like strings, numbers, and booleans)
* @param retry retry the request in case a 401 (unauthenticated) error is returned. Don't modify this.
* @returns APIError or an empty promise, in case something went wrong
*/
async event(
name: string,
hit: Hit,
duration: number = 0,
meta: Object | null = null,
retry: boolean = true
): Promise<APIError | null> {
try {
if (hit.dnt === '1') {
return Promise.resolve<null>(null)
}
const postAction = await this.client.post(
eventEndpoint,
{
hostname: this.hostname,
event_name: name,
event_duration: duration,
event_meta: meta,
...hit
},
{
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${this.accessToken}`
}
}
)
if (!postAction.ok && retry) {
try {
await this.refreshToken()
return this.event(name, hit, duration, meta, false)
} catch (e) {
return e as APIError
}
}
return Promise.resolve<null>(null)
} catch (e: any) {
return Promise.reject(e)
}
}
/**
* hitFromRequest returns the required data to send a hit to Pirsch for a Node request object.
*
* @param req the Node request object from the http package.
* @returns Hit object containing all necessary fields.
*/
hitFromRequest(req: IncomingMessage): Hit {
const url = new URL(req.url || '', `${this.protocol}://${this.hostname}`)
return {
url: url.toString(),
ip: req.socket.remoteAddress || '',
cf_connecting_ip: (req.headers['cf-connecting-ip'] as string) || '',
x_forwarded_for: (req.headers['x-forwarded-for'] as string) || '',
forwarded: req.headers['forwarded'] || '',
x_real_ip: (req.headers['x-real-ip'] as string) || '',
dnt: (req.headers['dnt'] as string) || '',
user_agent: req.headers['user-agent'] || '',
accept_language: req.headers['accept-language'] || '',
referrer: Client.getReferrer(req, url)
}
}
hitFromNextRequest(req: NextRequest) {
const url = new URL(req.url || '', `${this.protocol}://${this.hostname}`)
const hit: Partial<Hit> = {
url: url.toString(),
ip: req.ip || '',
cf_connecting_ip: (req.headers.get('cf-connecting-ip') as string) || '',
x_forwarded_for: (req.headers.get('x-forwarded-for') as string) || '',
forwarded: req.headers.get('forwarded') || '',
x_real_ip: (req.headers.get('x-real-ip') as string) || '',
dnt: (req.headers.get('dnt') as string) || '',
user_agent: req.headers.get('user-agent') || '',
accept_language: req.headers.get('accept-language') || '',
referrer: Client.getReferrer(req, url)
}
return hit as Hit
}
private async refreshToken(): Promise<APIError | null> {
try {
const resp = await this.client.post(authenticationEndpoint, {
client_id: this.clientID,
client_secret: this.clientSecret
})
const responseBody = (await resp.json()) as unknown as AuthenticationResponse
this.accessToken = responseBody.access_token
return Promise.resolve<null>(null)
} catch (e: any) {
this.accessToken = ''
if (e.response) {
return Promise.reject<APIError>(e.response.data)
}
return Promise.reject(e)
}
}
static getReferrer(req: IncomingMessage | NextRequest, url: URL): string {
// @ts-expect-error we know this header exists
let referrer = req.headers['referer'] || ''
if (referrer === '') {
for (let ref of referrerQueryParams) {
const param = url.searchParams.get(ref)
if (param && param !== '') {
return param
}
}
}
return referrer
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment