-
-
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