Skip to content

Instantly share code, notes, and snippets.

@zanona
Last active February 9, 2026 23:32
Show Gist options
  • Select an option

  • Save zanona/0f3d42093eaa8ac5c33286cc7eca1166 to your computer and use it in GitHub Desktop.

Select an option

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);
}
@abdulaziz-mohammed

Copy link
Copy Markdown

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

zanona commented Jan 28, 2022

Copy link
Copy Markdown
Author

@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

razbakov commented Feb 2, 2022

Copy link
Copy Markdown

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

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

@abierbaum

Copy link
Copy Markdown

@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

zanona commented Jan 12, 2023

Copy link
Copy Markdown
Author

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

JFGHT commented Dec 29, 2023

Copy link
Copy Markdown

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

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

@zanona

zanona commented Jan 6, 2024

Copy link
Copy Markdown
Author

Awesome work @JFGHT! Happy new Year!

@iotashan

iotashan commented Jun 8, 2024

Copy link
Copy Markdown

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

https://firebase.google.com/docs/functions/2nd-gen-upgrade

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