-
-
Save JFGHT/32cb01e9b3e842579dd2cc2741d2033e to your computer and use it in GitHub Desktop.
Missing Sentry's firebase serverless wrappers
This file contains 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
/** | |
* Temporary wrapper for firebase functions until @sentry/serverless support is implemented | |
* It currently supports wrapping https, pubsub and firestore handlers. | |
* usage: https.onRequest(wrap((req, res) => {...})) | |
* Updated by JFGHT 29/12/2023 | |
* Taken from https://gist.github.com/zanona/0f3d42093eaa8ac5c33286cc7eca1166 | |
*/ | |
import type { Event } from '@sentry/types'; | |
import type { https } from 'firebase-functions'; | |
import type { onRequest, onCall } from 'firebase-functions/lib/providers/https'; | |
import type { ScheduleBuilder } from 'firebase-functions/lib/providers/pubsub'; | |
import type { DocumentBuilder } from 'firebase-functions/lib/providers/firestore'; | |
type httpsOnRequestHandler = Parameters<typeof onRequest>[0]; | |
type httpsOnCallHandler = Parameters<typeof onCall>[0]; | |
type pubsubOnRunHandler = Parameters<ScheduleBuilder['onRun']>[0]; | |
type firestoreOnWriteHandler = Parameters<DocumentBuilder['onWrite']>[0]; | |
type firestoreOnUpdateHandler = Parameters<DocumentBuilder['onUpdate']>[0]; | |
type firestoreOnCreateHandler = Parameters<DocumentBuilder['onCreate']>[0]; | |
type firestoreOnDeleteHandler = Parameters<DocumentBuilder['onDelete']>[0]; | |
type FunctionType = 'http' | 'callable' | 'document' | 'schedule'; | |
export function getLocationHeaders(req: https.Request): { country?: string; ip?: string } { | |
/** | |
* Checking order: | |
* Cloudflare: in case user is proxying functions through it | |
* Fastly: in case user is service functions through firebase hosting (Fastly is the default Firebase CDN) | |
* App Engine: in case user is serving functions directly through cloudfunctions.net | |
*/ | |
const ip = | |
req.header('Cf-Connecting-Ip') || | |
req.header('Fastly-Client-Ip') || | |
req.header('X-Appengine-User-Ip') || | |
req.header('X-Forwarded-For')?.split(',')[0] || | |
req.connection.remoteAddress || | |
req.socket.remoteAddress; | |
const country = | |
req.header('Cf-Ipcountry') || | |
req.header('X-Country-Code') || | |
req.header('X-Appengine-Country'); | |
return { ip: ip?.toString(), country: country?.toString() }; | |
} | |
function wrap<A, C>(type: FunctionType, name: string, fn: (a: A) => C | Promise<C>): typeof fn; | |
function wrap<A, B, C>( | |
type: FunctionType, | |
name: string, | |
fn: (a: A, b: B) => C | Promise<C> | |
): typeof fn; | |
function wrap<A, B, C>( | |
type: FunctionType, | |
name: string, | |
fn: (a: A, b: B) => C | Promise<C>, | |
): typeof fn { | |
// Don't wrap functions when running locally | |
if (process.env.FUNCTIONS_EMULATOR) { | |
return fn; | |
} | |
return async (a: A, b: B): Promise<C> => { | |
const { | |
startTransaction, captureException, flush, addRequestDataToEvent, | |
getCurrentScope, extractTraceparentData, | |
} = await import('@sentry/node'); | |
let req: https.Request | undefined; | |
let ctx: Record<string, unknown> | undefined; | |
if (type === 'http') { | |
req = (a as unknown) as https.Request; | |
} | |
if (type === 'callable') { | |
const ctxLocal = (b as unknown) as https.CallableContext; | |
req = ctxLocal.rawRequest; | |
} | |
if (type === 'document') { | |
ctx = (b as unknown) as Record<string, unknown>; | |
} | |
if (type === 'schedule') { | |
ctx = (a as unknown) as Record<string, unknown>; | |
} | |
const traceparentData = extractTraceparentData(req?.header('sentry-trace') || ''); | |
const transaction = startTransaction({ | |
name, | |
op: 'transaction', | |
...traceparentData, | |
}); | |
const scope = getCurrentScope(); | |
scope.addEventProcessor((event): Event => { | |
let ev: Event = event; | |
if (req) { | |
ev = addRequestDataToEvent(event, req); | |
const loc = getLocationHeaders(req); | |
if (loc.ip) { | |
ev.user = { ...ev.user, ip_address: loc.ip }; | |
} | |
if (loc.country) { | |
ev.user = { ...ev.user, country: loc.country }; | |
} | |
} | |
if (ctx) { | |
ev = addRequestDataToEvent(event, ctx); | |
ev.extra = ctx; | |
delete ev.request; | |
} | |
ev.transaction = transaction.name; | |
// force catpuring uncaughtError as not handled | |
const mechanism = ev.exception?.values?.[0].mechanism; | |
if (mechanism && ev.tags?.handled === false) { | |
mechanism.handled = false; | |
} | |
return ev; | |
}); | |
scope.setSpan(transaction); | |
return Promise.resolve(fn(a, b)) | |
.catch((err): void => { | |
captureException(err, { tags: { handled: false } }); | |
throw err; | |
}) | |
.finally((): Promise<boolean> => { | |
transaction.finish(); | |
return flush(2000); | |
}) as Promise<C | undefined>; | |
}; | |
} | |
export function wrapHttpsOnRequestHandler(name: string, fn: httpsOnRequestHandler): typeof fn { | |
return wrap('http', name, fn); | |
} | |
export function wrapHttpsOnCallHandler(name: string, fn: httpsOnCallHandler): typeof fn { | |
return wrap('callable', name, fn); | |
} | |
export function wrapPubsubOnRunHandler(name: string, fn: pubsubOnRunHandler): typeof fn { | |
return wrap('schedule', name, fn); | |
} | |
export function wrapFirestoreOnWriteHandler(name: string, fn: firestoreOnWriteHandler): typeof fn { | |
return wrap('document', name, fn); | |
} | |
export function wrapFirestoreOnUpdateHandler( | |
name: string, | |
fn: firestoreOnUpdateHandler, | |
): typeof fn { | |
return wrap('document', name, fn); | |
} | |
export function wrapFirestoreOnCreateHandler( | |
name: string, | |
fn: firestoreOnCreateHandler, | |
): typeof fn { | |
return wrap('document', name, fn); | |
} | |
export function wrapFirestoreOnDeleteHandler( | |
name: string, | |
fn: firestoreOnDeleteHandler, | |
): typeof fn { | |
return wrap('document', name, fn); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment