-
-
Save zanona/0f3d42093eaa8ac5c33286cc7eca1166 to your computer and use it in GitHub Desktop.
| /** | |
| * 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) => {...})) | |
| */ | |
| 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 { | |
| return async (a: A, b: B): Promise<C> => { | |
| const {startTransaction, configureScope, Handlers, captureException, flush} = await import( | |
| '@sentry/node' | |
| ); | |
| const {extractTraceparentData} = await import('@sentry/tracing'); | |
| 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, | |
| }); | |
| configureScope(scope => { | |
| scope.addEventProcessor(event => { | |
| let ev: Event = event; | |
| if (req) { | |
| ev = Handlers.parseRequest(event, req); | |
| const loc = getLocationHeaders(req); | |
| loc.ip && Object.assign(ev.user, {ip_address: loc.ip}); | |
| loc.country && Object.assign(ev.user, {country: loc.country}); | |
| } | |
| if (ctx) { | |
| ev = Handlers.parseRequest(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 => { | |
| captureException(err, {tags: {handled: false}}); | |
| throw err; | |
| }) | |
| .finally(() => { | |
| transaction.finish(); | |
| return flush(2000); | |
| }); | |
| }; | |
| } | |
| 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); | |
| } |
@razbakov import '@sentry/tracing'; and place "SentryTracing.addExtensionMethods();" in the code.
@zanona or anyone else, have you tried porting these ideas over to cloud functions v2? I would love to use them there as well.
Hey, @abierbaum. I haven't yet used v2 functions. Would you happen to know what have changed from the previous implementation which would prevent this to work?
I have updated this gist removing deprecations and avoiding the wrap in localhost.
https://gist.github.com/JFGHT/32cb01e9b3e842579dd2cc2741d2033e
Awesome work @JFGHT! Happy new Year!
Hey, @abierbaum. I haven't yet used v2 functions. Would you happen to know what have changed from the previous implementation which would prevent this to work?
@zanona different library, different method signatures:
import {onRequest} from "firebase-functions/v2/https";
Wow! Amazing!
But I am getting this error:
TS2339: Property 'finally' does not exist on type 'Promise'.
I am using typescript 3.9.10