Skip to content

Instantly share code, notes, and snippets.

@zanona
Last active April 3, 2024 15:48
Show Gist options
  • Star 25 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save zanona/0f3d42093eaa8ac5c33286cc7eca1166 to your computer and use it in GitHub Desktop.
Save zanona/0f3d42093eaa8ac5c33286cc7eca1166 to your computer and use it in GitHub Desktop.
Missing Sentry's firebase serverless wrappers
/**
* 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);
}
@zeevl
Copy link

zeevl commented Mar 18, 2021

This is great, thank you for posting!!

Any chance you could include the source for ../types as well?

@zanona
Copy link
Author

zanona commented Mar 18, 2021

Hi, @zeevl. Sure, I just updated to the latest version I'm using which adds IP and Country data to the context.
Types are also embedded now.

@zeevl
Copy link

zeevl commented Mar 18, 2021

Thank you!!!

@gregdingle
Copy link

Thanks for posting this. I asked about firebase coverage last week in Sentry's Discord community and the response I got was "We don't have it scheduled, but our PRs are open".

@gregdingle
Copy link

To sum up the benefits over merely wrapping with captureException:

  • transactions
  • spans
  • distributed tracing
  • request context
  • typing

@gregdingle
Copy link

Question: why are @sentry/node and @sentry/tracing imported dynamically here https://gist.github.com/zanona/0f3d42093eaa8ac5c33286cc7eca1166#file-sentry-serverless-firebase-ts-L56 ?

@zanona
Copy link
Author

zanona commented May 25, 2021

Hi, @gregdingle. I'm glad you found it useful.
In regard to dynamic imports, I do remember that at the time, static imports were affecting cold start times and, as I had some other functions which didn't need to be covered by sentry, I decided to lazily import those dependencies.
https://youtu.be/v3eG9xpzNXM?t=174

@n-sviridenko
Copy link

Great job 💪

@n-sviridenko
Copy link

Screenshot 2021-08-17 at 18 23 26

Am I right btw? Or it was a typo and we have to remove const?

@zanona
Copy link
Author

zanona commented Aug 18, 2021

Am I right btw? Or it was a typo and we have to remove const?

@n-sviridenko Well spotted! I think your solution with ctxLocal is pretty good. I just updated the gist.

@n-sviridenko
Copy link

Screenshot 2021-09-01 at 16 49 37

Continuing my series of adjustments. parseRequest expects an express request, while ctx is smth else and you already have parseRequest applied on the line 103.

@abdulaziz-mohammed
Copy link

Hi @zanona, Great work!
I'm not sure but I think line #115 is unnecessary since it causes to fail every request when Sentry starts to throw 429 Errors (due to rate-limiting or plan limit reached).

@zanona
Copy link
Author

zanona commented Jan 28, 2022

@abdulaziz-mohammed thanks for the feedback.
Would you be able to think of any other implications while removing line 115?
At the moment it comes to mind that, if we don't throw an error, those will never be signalled on Firebase console? Causing all executions finish successfully?

I might be wrong, though. What do you think?

@razbakov
Copy link

razbakov commented Feb 2, 2022

Wow! Amazing!

But I am getting this error:
TS2339: Property 'finally' does not exist on type 'Promise'.

I am using typescript 3.9.10

@pavan168
Copy link

@razbakov import '@sentry/tracing'; and place "SentryTracing.addExtensionMethods();" in the code.

@abierbaum
Copy link

@zanona or anyone else, have you tried porting these ideas over to cloud functions v2? I would love to use them there as well.

@zanona
Copy link
Author

zanona commented Jan 12, 2023

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?

@JFGHT
Copy link

JFGHT commented Dec 29, 2023

I have updated this gist removing deprecations and avoiding the wrap in localhost.

https://gist.github.com/JFGHT/32cb01e9b3e842579dd2cc2741d2033e

@zanona
Copy link
Author

zanona commented Jan 6, 2024

Awesome work @JFGHT! Happy new Year!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment