Last active
November 4, 2021 11:38
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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