From 7b72760ffa182ff23149b2dfeb53725122796582 Mon Sep 17 00:00:00 2001 From: JPeer264 Date: Thu, 16 Oct 2025 11:12:07 +0200 Subject: [PATCH 1/8] feat(firebase): instrument cloud functions for firebase v2 --- packages/node/package.json | 1 + .../integrations/tracing/firebase/firebase.ts | 5 + .../firebase/otel/firebaseInstrumentation.ts | 3 + .../firebase/otel/patches/functions.ts | 332 ++++++++++++++++++ .../tracing/firebase/otel/types.ts | 5 + yarn.lock | 105 ++++++ 6 files changed, 451 insertions(+) create mode 100644 packages/node/src/integrations/tracing/firebase/otel/patches/functions.ts diff --git a/packages/node/package.json b/packages/node/package.json index a78a670a6cdd..2d73d82405df 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -98,6 +98,7 @@ "@sentry/core": "10.20.0", "@sentry/node-core": "10.20.0", "@sentry/opentelemetry": "10.20.0", + "firebase-functions": "^6.5.0", "import-in-the-middle": "^1.14.2", "minimatch": "^9.0.0" }, diff --git a/packages/node/src/integrations/tracing/firebase/firebase.ts b/packages/node/src/integrations/tracing/firebase/firebase.ts index 649a7089289b..8429f3a15597 100644 --- a/packages/node/src/integrations/tracing/firebase/firebase.ts +++ b/packages/node/src/integrations/tracing/firebase/firebase.ts @@ -11,6 +11,11 @@ const config: FirebaseInstrumentationConfig = { span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'db.query'); }, + functionsSpanCreationHook: span => { + addOriginToSpan(span, 'auto.firebase.otel.functions'); + + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'http.request'); + }, }; export const instrumentFirebase = generateInstrumentOnce(INTEGRATION_NAME, () => new FirebaseInstrumentation(config)); diff --git a/packages/node/src/integrations/tracing/firebase/otel/firebaseInstrumentation.ts b/packages/node/src/integrations/tracing/firebase/otel/firebaseInstrumentation.ts index ad67ea701079..724005e6f9ed 100644 --- a/packages/node/src/integrations/tracing/firebase/otel/firebaseInstrumentation.ts +++ b/packages/node/src/integrations/tracing/firebase/otel/firebaseInstrumentation.ts @@ -1,10 +1,12 @@ import { type InstrumentationNodeModuleDefinition, InstrumentationBase } from '@opentelemetry/instrumentation'; import { SDK_VERSION } from '@sentry/core'; import { patchFirestore } from './patches/firestore'; +import { patchFunctions } from './patches/functions'; import type { FirebaseInstrumentationConfig } from './types'; const DefaultFirebaseInstrumentationConfig: FirebaseInstrumentationConfig = {}; const firestoreSupportedVersions = ['>=3.0.0 <5']; // firebase 9+ +const functionsSupportedVersions = ['>=6.0.0 <7']; // firebase-functions v2 /** * Instrumentation for Firebase services, specifically Firestore. @@ -31,6 +33,7 @@ export class FirebaseInstrumentation extends InstrumentationBase {}; + + let functionsSpanCreationHook: FunctionsSpanCreationHook = defaultFunctionsSpanCreationHook; + const configFunctionsSpanCreationHook = config.functionsSpanCreationHook; + + if (typeof configFunctionsSpanCreationHook === 'function') { + functionsSpanCreationHook = (span: Span) => { + safeExecuteInTheMiddle( + () => configFunctionsSpanCreationHook(span), + error => { + if (!error) { + return; + } + diag.error(error?.message); + }, + true, + ); + }; + } + + const moduleFunctionsCJS = new InstrumentationNodeModuleDefinition('firebase-functions', functionsSupportedVersions); + + moduleFunctionsCJS.files.push( + new InstrumentationNodeModuleFile( + 'firebase-functions/lib/v2/providers/https.js', + functionsSupportedVersions, + moduleExports => wrapCommonFunctions(moduleExports, wrap, unwrap, tracer, functionsSpanCreationHook, 'function'), + moduleExports => unwrapCommonFunctions(moduleExports, unwrap), + ), + ); + + moduleFunctionsCJS.files.push( + new InstrumentationNodeModuleFile( + 'firebase-functions/lib/v2/providers/firestore.js', + functionsSupportedVersions, + moduleExports => wrapCommonFunctions(moduleExports, wrap, unwrap, tracer, functionsSpanCreationHook, 'firestore'), + moduleExports => unwrapCommonFunctions(moduleExports, unwrap), + ), + ); + + moduleFunctionsCJS.files.push( + new InstrumentationNodeModuleFile( + 'firebase-functions/lib/v2/providers/scheduler.js', + functionsSupportedVersions, + moduleExports => wrapCommonFunctions(moduleExports, wrap, unwrap, tracer, functionsSpanCreationHook, 'scheduler'), + moduleExports => unwrapCommonFunctions(moduleExports, unwrap), + ), + ); + + moduleFunctionsCJS.files.push( + new InstrumentationNodeModuleFile( + 'firebase-functions/lib/v2/storage.js', + functionsSupportedVersions, + moduleExports => wrapCommonFunctions(moduleExports, wrap, unwrap, tracer, functionsSpanCreationHook, 'storage'), + moduleExports => unwrapCommonFunctions(moduleExports, unwrap), + ), + ); + + return moduleFunctionsCJS; +} + +type OverloadedParameters = T extends { + (...args: infer A1): unknown; + (...args: infer A2): unknown; + (...args: infer A3): unknown; + (...args: infer A4): unknown; +} + ? A1 | A2 | A3 | A4 + : T extends { (...args: infer A1): unknown; (...args: infer A2): unknown; (...args: infer A3): unknown } + ? A1 | A2 | A3 + : T extends { (...args: infer A1): unknown; (...args: infer A2): unknown } + ? A1 | A2 + : T extends (...args: infer A) => unknown + ? A + : unknown; + +type AvailableFirebaseFunctions = { + onRequest: typeof onRequest; + onCall: typeof onCall; + onDocumentCreated: typeof onDocumentCreated; + onDocumentUpdated: typeof onDocumentUpdated; + onDocumentDeleted: typeof onDocumentDeleted; + onDocumentWritten: typeof onDocumentWritten; + onDocumentCreatedWithAuthContext: typeof onDocumentCreatedWithAuthContext; + onDocumentUpdatedWithAuthContext: typeof onDocumentUpdatedWithAuthContext; + onDocumentDeletedWithAuthContext: typeof onDocumentDeletedWithAuthContext; + onDocumentWrittenWithAuthContext: typeof onDocumentWrittenWithAuthContext; + onSchedule: typeof onSchedule; + onObjectFinalized: typeof onObjectFinalized; + onObjectArchived: typeof onObjectArchived; + onObjectDeleted: typeof onObjectDeleted; + onObjectMetadataUpdated: typeof onObjectMetadataUpdated; +}; + +type FirebaseFunctions = AvailableFirebaseFunctions[keyof AvailableFirebaseFunctions]; + +/** + * Patches Cloud Functions for Firebase (v2) to add OpenTelemetry instrumentation + * + * @param tracer - Opentelemetry Tracer + * @param functionsSpanCreationHook - Function to create a span for the function + * @param triggerType - Type of trigger + * @returns A function that patches the function + */ +export function patchV2Functions( + tracer: Tracer, + functionsSpanCreationHook: FunctionsSpanCreationHook, + triggerType: string, +): (original: T) => (...args: OverloadedParameters) => ReturnType { + return function v2FunctionsWrapper(original: T): (...args: OverloadedParameters) => ReturnType { + return function (this: FirebaseInstrumentation, ...args: OverloadedParameters): ReturnType { + const handler = typeof args[0] === 'function' ? args[0] : args[1]; + const documentOrOptions = typeof args[0] === 'function' ? undefined : args[0]; + + if (!handler) { + return original.call(this, ...args); + } + + const wrappedHandler = async function (this: unknown, ...handlerArgs: unknown[]): Promise { + const functionName = process.env.FUNCTION_TARGET || process.env.K_SERVICE || 'unknown'; + const span = tracer.startSpan(`firebase.function.${triggerType}`, { + kind: SpanKind.SERVER, + }); + + const attributes: SpanAttributes = { + 'faas.name': functionName, + 'faas.trigger': triggerType, + 'faas.provider': 'firebase', + }; + + if (process.env.GCLOUD_PROJECT) { + attributes['cloud.project_id'] = process.env.GCLOUD_PROJECT; + } + + if (process.env.EVENTARC_CLOUD_EVENT_SOURCE) { + attributes['cloud.event_source'] = process.env.EVENTARC_CLOUD_EVENT_SOURCE; + } + + span.setAttributes(attributes); + functionsSpanCreationHook(span); + + return context.with(trace.setSpan(context.active(), span), () => + safeExecuteInTheMiddleAsync( + () => handler.apply(this, handlerArgs), + err => { + if (err instanceof Error) { + span.recordException(err); + } + + span.end(); + }, + ), + ); + }; + + if (documentOrOptions) { + return original.call(this, documentOrOptions, wrappedHandler); + } else { + return original.call(this, wrappedHandler); + } + }; + }; +} + +function wrapCommonFunctions( + moduleExports: AvailableFirebaseFunctions, + wrap: InstrumentationBase['_wrap'], + unwrap: InstrumentationBase['_unwrap'], + tracer: Tracer, + functionsSpanCreationHook: FunctionsSpanCreationHook, + triggerType: 'function' | 'firestore' | 'scheduler' | 'storage', +): AvailableFirebaseFunctions { + unwrapCommonFunctions(moduleExports, unwrap); + + switch (triggerType) { + case 'function': + wrap(moduleExports, 'onRequest', patchV2Functions(tracer, functionsSpanCreationHook, 'http.request')); + wrap(moduleExports, 'onCall', patchV2Functions(tracer, functionsSpanCreationHook, 'http.call')); + break; + + case 'firestore': + wrap( + moduleExports, + 'onDocumentCreated', + patchV2Functions(tracer, functionsSpanCreationHook, 'firestore.document.created'), + ); + wrap( + moduleExports, + 'onDocumentUpdated', + patchV2Functions(tracer, functionsSpanCreationHook, 'firestore.document.updated'), + ); + wrap( + moduleExports, + 'onDocumentDeleted', + patchV2Functions(tracer, functionsSpanCreationHook, 'firestore.document.deleted'), + ); + wrap( + moduleExports, + 'onDocumentWritten', + patchV2Functions(tracer, functionsSpanCreationHook, 'firestore.document.written'), + ); + wrap( + moduleExports, + 'onDocumentCreatedWithAuthContext', + patchV2Functions(tracer, functionsSpanCreationHook, 'firestore.document.created'), + ); + wrap( + moduleExports, + 'onDocumentUpdatedWithAuthContext', + patchV2Functions(tracer, functionsSpanCreationHook, 'firestore.document.updated'), + ); + + wrap( + moduleExports, + 'onDocumentDeletedWithAuthContext', + patchV2Functions(tracer, functionsSpanCreationHook, 'firestore.document.deleted'), + ); + + wrap( + moduleExports, + 'onDocumentWrittenWithAuthContext', + patchV2Functions(tracer, functionsSpanCreationHook, 'firestore.document.written'), + ); + break; + + case 'scheduler': + wrap(moduleExports, 'onSchedule', patchV2Functions(tracer, functionsSpanCreationHook, 'scheduler.scheduled')); + break; + + case 'storage': + wrap( + moduleExports, + 'onObjectFinalized', + patchV2Functions(tracer, functionsSpanCreationHook, 'storage.object.finalized'), + ); + wrap( + moduleExports, + 'onObjectArchived', + patchV2Functions(tracer, functionsSpanCreationHook, 'storage.object.archived'), + ); + wrap( + moduleExports, + 'onObjectDeleted', + patchV2Functions(tracer, functionsSpanCreationHook, 'storage.object.deleted'), + ); + wrap( + moduleExports, + 'onObjectMetadataUpdated', + patchV2Functions(tracer, functionsSpanCreationHook, 'storage.object.metadataUpdated'), + ); + break; + } + + return moduleExports; +} + +function unwrapCommonFunctions( + moduleExports: AvailableFirebaseFunctions, + unwrap: InstrumentationBase['_unwrap'], +): AvailableFirebaseFunctions { + const methods: (keyof AvailableFirebaseFunctions)[] = [ + 'onSchedule', + 'onRequest', + 'onCall', + 'onObjectFinalized', + 'onObjectArchived', + 'onObjectDeleted', + 'onObjectMetadataUpdated', + 'onDocumentCreated', + 'onDocumentUpdated', + 'onDocumentDeleted', + 'onDocumentWritten', + 'onDocumentCreatedWithAuthContext', + 'onDocumentUpdatedWithAuthContext', + 'onDocumentDeletedWithAuthContext', + 'onDocumentWrittenWithAuthContext', + ]; + + for (const method of methods) { + if (isWrapped(moduleExports[method])) { + unwrap(moduleExports, method); + } + } + return moduleExports; +} diff --git a/packages/node/src/integrations/tracing/firebase/otel/types.ts b/packages/node/src/integrations/tracing/firebase/otel/types.ts index ecc48bc09498..aa48d4c8fec2 100644 --- a/packages/node/src/integrations/tracing/firebase/otel/types.ts +++ b/packages/node/src/integrations/tracing/firebase/otel/types.ts @@ -88,12 +88,17 @@ export interface FirestoreSettings { */ export interface FirebaseInstrumentationConfig extends InstrumentationConfig { firestoreSpanCreationHook?: FirestoreSpanCreationHook; + functionsSpanCreationHook?: FunctionsSpanCreationHook; } export interface FirestoreSpanCreationHook { (span: Span): void; } +export interface FunctionsSpanCreationHook { + (span: Span): void; +} + // Function types (addDoc, getDocs, setDoc, deleteDoc) are defined below as types export type GetDocsType = ( query: CollectionReference, diff --git a/yarn.lock b/yarn.lock index 70c9e3d80b73..a2562cbe0cfa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8272,6 +8272,13 @@ resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.12.tgz#6b2c510a7ad7039e98e7b8d3d6598f4359e5c080" integrity sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw== +"@types/cors@^2.8.5": + version "2.8.19" + resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.19.tgz#d93ea2673fd8c9f697367f5eeefc2bbfa94f0342" + integrity sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg== + dependencies: + "@types/node" "*" + "@types/css-font-loading-module@0.0.7": version "0.0.7" resolved "https://registry.yarnpkg.com/@types/css-font-loading-module/-/css-font-loading-module-0.0.7.tgz#2f98ede46acc0975de85c0b7b0ebe06041d24601" @@ -8534,6 +8541,16 @@ "@types/qs" "*" "@types/serve-static" "*" +"@types/express@^4.17.21": + version "4.17.23" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.23.tgz#35af3193c640bfd4d7fe77191cd0ed411a433bef" + integrity sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.33" + "@types/qs" "*" + "@types/serve-static" "*" + "@types/fs-extra@^5.0.5": version "5.1.0" resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-5.1.0.tgz#2a325ef97901504a3828718c390d34b8426a10a1" @@ -8780,6 +8797,13 @@ dependencies: undici-types "~6.20.0" +"@types/node@>=13.7.0": + version "24.7.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-24.7.2.tgz#5adf66b6e2ac5cab1d10a2ad3682e359cb652f4a" + integrity sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA== + dependencies: + undici-types "~7.14.0" + "@types/node@^10.1.0": version "10.17.60" resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.60.tgz#35f3d6213daed95da7f0f73e75bcc6980e90597b" @@ -16751,6 +16775,43 @@ express@^4.10.7, express@^4.17.1, express@^4.17.3, express@^4.18.1, express@^4.2 utils-merge "1.0.1" vary "~1.1.2" +express@^4.21.0: + version "4.21.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.21.2.tgz#cf250e48362174ead6cea4a566abef0162c1ec32" + integrity sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.3" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.7.1" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~2.0.0" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.3.1" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.3" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.12" + proxy-addr "~2.0.7" + qs "6.13.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.19.0" + serve-static "1.16.2" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + exsolve@^1.0.4, exsolve@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/exsolve/-/exsolve-1.0.7.tgz#3b74e4c7ca5c5f9a19c3626ca857309fa99f9e9e" @@ -17278,6 +17339,17 @@ findup-sync@^4.0.0: micromatch "^4.0.2" resolve-dir "^1.0.1" +firebase-functions@^6.5.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/firebase-functions/-/firebase-functions-6.5.0.tgz#0324babc47afbc4e10a3a5b9b289bd0f763e6ff8" + integrity sha512-ffntJkq88K0pdLDq14IyetKQu99Je4vBBJBe9rkZK2X4QyeJJnmA527+v2iTKA+SwRtxf5lh7sXgxpvxGS8HtQ== + dependencies: + "@types/cors" "^2.8.5" + "@types/express" "^4.17.21" + cors "^2.8.5" + express "^4.21.0" + protobufjs "^7.2.2" + fireworm@^0.7.2: version "0.7.2" resolved "https://registry.yarnpkg.com/fireworm/-/fireworm-0.7.2.tgz#bc5736515b48bd30bf3293a2062e0b0e0361537a" @@ -21330,6 +21402,11 @@ long@^4.0.0: resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== +long@^5.0.0: + version "5.3.2" + resolved "https://registry.yarnpkg.com/long/-/long-5.3.2.tgz#1d84463095999262d7d7b7f8bfd4a8cc55167f83" + integrity sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA== + long@^5.2.1: version "5.2.3" resolved "https://registry.yarnpkg.com/long/-/long-5.2.3.tgz#a3ba97f3877cf1d778eccbcb048525ebb77499e1" @@ -24752,6 +24829,11 @@ path-to-regexp@0.1.10: resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.10.tgz#67e9108c5c0551b9e5326064387de4763c4d5f8b" integrity sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w== +path-to-regexp@0.1.12: + version "0.1.12" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz#d5e1a12e478a976d432ef3c58d534b9923164bb7" + integrity sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ== + path-to-regexp@3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-3.3.0.tgz#f7f31d32e8518c2660862b644414b6d5c63a611b" @@ -26031,6 +26113,24 @@ property-information@^6.0.0: resolved "https://registry.yarnpkg.com/property-information/-/property-information-6.3.0.tgz#ba4a06ec6b4e1e90577df9931286953cdf4282c3" integrity sha512-gVNZ74nqhRMiIUYWGQdosYetaKc83x8oT41a0LlV3AAFCAZwCpg4vmGkq8t34+cUhp3cnM4XDiU/7xlgK7HGrg== +protobufjs@^7.2.2: + version "7.5.4" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.5.4.tgz#885d31fe9c4b37f25d1bb600da30b1c5b37d286a" + integrity sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg== + dependencies: + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.4" + "@protobufjs/eventemitter" "^1.1.0" + "@protobufjs/fetch" "^1.1.0" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.0" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.0" + "@types/node" ">=13.7.0" + long "^5.0.0" + protocols@^2.0.0, protocols@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/protocols/-/protocols-2.0.1.tgz#8f155da3fc0f32644e83c5782c8e8212ccf70a86" @@ -30204,6 +30304,11 @@ undici-types@~6.20.0: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433" integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg== +undici-types@~7.14.0: + version "7.14.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.14.0.tgz#4c037b32ca4d7d62fae042174604341588bc0840" + integrity sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA== + undici@^5.25.4, undici@^5.28.5: version "5.29.0" resolved "https://registry.yarnpkg.com/undici/-/undici-5.29.0.tgz#419595449ae3f2cdcba3580a2e8903399bd1f5a3" From ea065418129419ce1df07246f8c42dc59ace1030 Mon Sep 17 00:00:00 2001 From: JPeer264 Date: Thu, 16 Oct 2025 11:29:14 +0200 Subject: [PATCH 2/8] feat(firebase): update e2e tests to use functions --- .../node-firebase/.firebaserc | 5 - .../test-applications/node-firebase/README.md | 80 +++++------- .../node-firebase/firebase.json | 9 +- .../node-firebase/firestore-app/package.json | 25 ++++ .../{ => firestore-app}/src/app.ts | 0 .../{ => firestore-app}/src/init.ts | 0 .../node-firebase/firestore-app/tsconfig.json | 8 ++ .../node-firebase/functions/package.json | 21 ++++ .../node-firebase/functions/src/index.ts | 46 +++++++ .../node-firebase/functions/src/init.ts | 10 ++ .../node-firebase/functions/tsconfig.json | 8 ++ .../node-firebase/package.json | 27 ++-- .../node-firebase/pnpm-workspace.yaml | 3 + .../node-firebase/tests/functions.test.ts | 117 ++++++++++++++++++ .../node-firebase/tsconfig.build.json | 4 - .../node-firebase/tsconfig.json | 6 +- 16 files changed, 290 insertions(+), 79 deletions(-) delete mode 100644 dev-packages/e2e-tests/test-applications/node-firebase/.firebaserc create mode 100644 dev-packages/e2e-tests/test-applications/node-firebase/firestore-app/package.json rename dev-packages/e2e-tests/test-applications/node-firebase/{ => firestore-app}/src/app.ts (100%) rename dev-packages/e2e-tests/test-applications/node-firebase/{ => firestore-app}/src/init.ts (100%) create mode 100644 dev-packages/e2e-tests/test-applications/node-firebase/firestore-app/tsconfig.json create mode 100644 dev-packages/e2e-tests/test-applications/node-firebase/functions/package.json create mode 100644 dev-packages/e2e-tests/test-applications/node-firebase/functions/src/index.ts create mode 100644 dev-packages/e2e-tests/test-applications/node-firebase/functions/src/init.ts create mode 100644 dev-packages/e2e-tests/test-applications/node-firebase/functions/tsconfig.json create mode 100644 dev-packages/e2e-tests/test-applications/node-firebase/pnpm-workspace.yaml create mode 100644 dev-packages/e2e-tests/test-applications/node-firebase/tests/functions.test.ts delete mode 100644 dev-packages/e2e-tests/test-applications/node-firebase/tsconfig.build.json diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/.firebaserc b/dev-packages/e2e-tests/test-applications/node-firebase/.firebaserc deleted file mode 100644 index 47e4665f6905..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-firebase/.firebaserc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "projects": { - "default": "sentry-firebase-e2e-test-f4ed3" - } -} diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/README.md b/dev-packages/e2e-tests/test-applications/node-firebase/README.md index e44ee12f5268..bd91bd5a872a 100644 --- a/dev-packages/e2e-tests/test-applications/node-firebase/README.md +++ b/dev-packages/e2e-tests/test-applications/node-firebase/README.md @@ -1,64 +1,50 @@ -## Assuming you already have installed docker desktop or orbstack etc. or any other docker software +
+ + Firebase + +
-### Enabling / authorising firebase emulator through docker +## Description -1. Run the docker +[Firebase](https://firebase.google.com/) starter repository with Cloud Functions for Firebase and Firestore. -```bash -pnpm docker -``` - -2. In new tab, enter the docker container by simply running +## Project setup -```bash -docker exec -it sentry-firebase bash +```sh +$ pnpm install ``` -3. Now inside docker container run +## Compile and run the project -```bash -firebase login +```sh +$ pnpm dev # builds the functions and firestore app +$ pnpm emulate +$ pnpm start # run the firestore app ``` -4. You should now see a long link to authenticate with google account, copy the link and open it using your browser -5. Choose the account you want to authenticate with -6. Once you do this you should be able to see something like "Firebase CLI Login Successful" -7. And inside docker container you should see something like "Success! Logged in as " -8. Now you can exit docker container - -```bash -exit -``` +## Run tests -9. Switch back to previous tab, stop the docker container (ctrl+c). -10. You should now be able to run the test, as you have correctly authenticated the firebase emulator +Either run the tests directly: -### Preparing data for CLI - -1. Please authorize the docker first - see the previous section -2. Once you do that you can generate .env file locally, to do that just run - -```bash -npm run createEnvFromConfig +```sh +$ pnpm test:build +$ pnpm test:assert ``` -3. It will create a new file called ".env" inside folder "docker" -4. View the file. There will be 2 params CONFIG_FIREBASE_TOOLS and CONFIG_UPDATE_NOTIFIER_FIREBASE_TOOLS. -5. Now inside the CLI create a new variable under the name CONFIG_FIREBASE_TOOLS and - CONFIG_UPDATE_NOTIFIER_FIREBASE_TOOLS - take values from mentioned .env file -6. File .env is ignored to avoid situation when developer after authorizing firebase with private account will - accidently push the tokens to github. -7. But if we want the users to still have some default to be used for authorisation (on their local development) it will - be enough to commit this file, we just have to authorize it with some "special" account. +Or run develop while running the tests directly against the emulator. Start each script in a separate terminal: -**Some explanation towards environment settings, the environment variable defined directly in "environments" takes -precedence over .env file, that means it will be safe to define it in CLI and still keeps the .env file.** +```sh +$ pnpm dev +$ pnpm emulate +$ pnpm test --ui +``` -### Scripts - helpers +The tests will run against the Firebase Emulator Suite. -- createEnvFromConfig - it will use the firebase docker authentication and create .env file which will be used then by - docker whenever you run emulator -- createConfigFromEnv - it will use '.env' file in docker folder to create .config for the firebase to be used to - authenticate whenever you run docker, Docker by default loads .env file itself +## Resources -Use these scripts when testing and updating the environment settings on CLI +- [Firebase](https://firebase.google.com/) +- [Firebase Emulator Suite](https://firebase.google.com/docs/emulator-suite) +- [Firebase SDK](https://firebase.google.com/docs/sdk) +- [Firebase Functions](https://firebase.google.com/docs/functions) +- [Firestore](https://firebase.google.com/docs/firestore) diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/firebase.json b/dev-packages/e2e-tests/test-applications/node-firebase/firebase.json index 05203f1d6567..eb1b42b8aa9c 100644 --- a/dev-packages/e2e-tests/test-applications/node-firebase/firebase.json +++ b/dev-packages/e2e-tests/test-applications/node-firebase/firebase.json @@ -16,5 +16,12 @@ "enabled": true }, "singleProjectMode": true - } + }, + "functions": [ + { + "source": "functions", + "codebase": "default", + "ignore": ["node_modules", ".git", "firebase-debug.log", "firebase-debug.*.log", "*.local"] + } + ] } diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/firestore-app/package.json b/dev-packages/e2e-tests/test-applications/node-firebase/firestore-app/package.json new file mode 100644 index 000000000000..434af16a99c2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-firebase/firestore-app/package.json @@ -0,0 +1,25 @@ +{ + "name": "firestore-app", + "private": true, + "scripts": { + "build": "tsc", + "dev": "tsc --build --watch", + "start": "node ./dist/app.js" + }, + "dependencies": { + "@firebase/app": "^0.13.1", + "@sentry/node": "latest || *", + "@sentry/core": "latest || *", + "@sentry/opentelemetry": "latest || *", + "@types/node": "^22.13.14", + "dotenv": "^16.4.5", + "express": "^4.18.2", + "firebase": "^12.0.0", + "firebase-admin": "^13.5.0", + "tsconfig-paths": "^4.2.0" + }, + "devDependencies": { + "@types/express": "^4.17.13", + "typescript": "5.9.3" + } +} diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/src/app.ts b/dev-packages/e2e-tests/test-applications/node-firebase/firestore-app/src/app.ts similarity index 100% rename from dev-packages/e2e-tests/test-applications/node-firebase/src/app.ts rename to dev-packages/e2e-tests/test-applications/node-firebase/firestore-app/src/app.ts diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/src/init.ts b/dev-packages/e2e-tests/test-applications/node-firebase/firestore-app/src/init.ts similarity index 100% rename from dev-packages/e2e-tests/test-applications/node-firebase/src/init.ts rename to dev-packages/e2e-tests/test-applications/node-firebase/firestore-app/src/init.ts diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/firestore-app/tsconfig.json b/dev-packages/e2e-tests/test-applications/node-firebase/firestore-app/tsconfig.json new file mode 100644 index 000000000000..ee180965030d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-firebase/firestore-app/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "skipLibCheck": true + }, + "include": ["src"] +} diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/functions/package.json b/dev-packages/e2e-tests/test-applications/node-firebase/functions/package.json new file mode 100644 index 000000000000..178305b8c279 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-firebase/functions/package.json @@ -0,0 +1,21 @@ +{ + "name": "functions", + "scripts": { + "build": "tsc", + "dev": "tsc --build --watch" + }, + "engines": { + "node": "20" + }, + "main": "dist/index.js", + "dependencies": { + "firebase-admin": "^12.6.0", + "firebase-functions": "^6.0.1", + "@sentry/node": "latest || *", + "@sentry/core": "latest || *", + "@sentry/opentelemetry": "latest || *" + }, + "devDependencies": { + "typescript": "5.9.3" + } +} diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/functions/src/index.ts b/dev-packages/e2e-tests/test-applications/node-firebase/functions/src/index.ts new file mode 100644 index 000000000000..a76dc6a0d2b1 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-firebase/functions/src/index.ts @@ -0,0 +1,46 @@ +import './init'; + +import { onDocumentCreated, onDocumentCreatedWithAuthContext } from 'firebase-functions/firestore'; +import { onCall, onRequest } from 'firebase-functions/https'; +import * as logger from 'firebase-functions/logger'; +import { setGlobalOptions } from 'firebase-functions/options'; +import * as admin from 'firebase-admin'; + +setGlobalOptions({ region: 'default' }); + +admin.initializeApp(); + +const db = admin.firestore(); + +export const helloWorld = onRequest(async (request, response) => { + logger.info('Hello logs!', { structuredData: true }); + + response.send('Hello from Firebase!'); +}); + +export const onCallSomething = onRequest(async (request, response) => { + const data = { + name: request.body?.name || 'Sample Document', + timestamp: performance.now(), + description: request.body?.description || 'Created via Cloud Function', + }; + + await db.collection('documents').add(data); + + logger.info('Create document!', { structuredData: true }); + + response.send({ message: 'Document created!' }); +}); + +export const onDocumentCreate = onDocumentCreated('documents/{documentId}', async event => { + const documentId = event.params.documentId; + + await db.collection('documents').doc(documentId).update({ + processed: true, + processedAt: new Date(), + }); +}); + +export const onDocumentCreateWithAuthContext = onDocumentCreatedWithAuthContext('documents/{documentId}', async () => { + // noop +}); diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/functions/src/init.ts b/dev-packages/e2e-tests/test-applications/node-firebase/functions/src/init.ts new file mode 100644 index 000000000000..c3b4a642375a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-firebase/functions/src/init.ts @@ -0,0 +1,10 @@ +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + integrations: [Sentry.firebaseIntegration()], + defaultIntegrations: false, + tunnel: `http://localhost:3031/`, // proxy server +}); diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/functions/tsconfig.json b/dev-packages/e2e-tests/test-applications/node-firebase/functions/tsconfig.json new file mode 100644 index 000000000000..ee180965030d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-firebase/functions/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "skipLibCheck": true + }, + "include": ["src"] +} diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/package.json b/dev-packages/e2e-tests/test-applications/node-firebase/package.json index 0a23fbbeef92..13bf9e6df667 100644 --- a/dev-packages/e2e-tests/test-applications/node-firebase/package.json +++ b/dev-packages/e2e-tests/test-applications/node-firebase/package.json @@ -3,34 +3,25 @@ "version": "0.0.1", "private": true, "scripts": { - "build": "tsc", - "dev": "tsc --build --watch", + "build": "pnpm run -r build", + "dev": "pnpm run -r dev", "proxy": "node start-event-proxy.mjs", - "emulate": "firebase emulators:start &", - "start": "node ./dist/app.js", + "emulate": "firebase emulators:start --project demo-functions", + "start": "pnpm run -r start", "test": "playwright test", - "clean": "npx rimraf node_modules pnpm-lock.yaml", + "clean": "npx rimraf node_modules **/node_modules pnpm-lock.yaml **/dist *-debug.log test-results", "test:build": "pnpm install && pnpm build", - "test:assert": "pnpm firebase emulators:exec 'pnpm test'" + "test:assert": "pnpm firebase emulators:exec --project demo-functions 'pnpm test'" }, "dependencies": { - "@firebase/app": "^0.13.1", - "@sentry/node": "latest || *", - "@sentry/core": "latest || *", - "@sentry/opentelemetry": "latest || *", - "@types/node": "^18.19.1", + "@types/node": "^22.13.14", "dotenv": "^16.4.5", - "express": "^4.18.2", - "firebase": "^12.0.0", - "firebase-admin": "^12.0.0", "tsconfig-paths": "^4.2.0", - "typescript": "4.9.5" + "typescript": "5.9.3" }, "devDependencies": { "@playwright/test": "~1.53.2", - "@sentry-internal/test-utils": "link:../../../test-utils", - "@types/express": "^4.17.13", - "firebase-tools": "^12.0.0" + "@sentry-internal/test-utils": "link:../../../test-utils" }, "volta": { "extends": "../../package.json" diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/pnpm-workspace.yaml b/dev-packages/e2e-tests/test-applications/node-firebase/pnpm-workspace.yaml new file mode 100644 index 000000000000..8a5eb172e019 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-firebase/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +packages: + - 'functions' + - 'firestore-app' diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/tests/functions.test.ts b/dev-packages/e2e-tests/test-applications/node-firebase/tests/functions.test.ts new file mode 100644 index 000000000000..19c92497c266 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-firebase/tests/functions.test.ts @@ -0,0 +1,117 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('should only call the function once without any extra calls', async () => { + const serverTransactionPromise = waitForTransaction('node-firebase', span => { + return span.transaction === 'firebase.function.http.request'; + }); + + await fetch(`http://localhost:5001/demo-functions/default/helloWorld`); + + const transactionEvent = await serverTransactionPromise; + + expect(transactionEvent.transaction).toEqual('firebase.function.http.request'); + expect(transactionEvent.contexts).toEqual( + expect.objectContaining({ + trace: expect.objectContaining({ + data: { + 'faas.name': 'helloWorld', + 'faas.provider': 'firebase', + 'faas.trigger': 'http.request', + 'otel.kind': 'SERVER', + 'sentry.op': 'http.request', + 'sentry.origin': 'auto.firebase.otel.functions', + 'sentry.sample_rate': expect.any(Number), + 'sentry.source': 'route', + }, + op: 'http.request', + origin: 'auto.firebase.otel.functions', + span_id: expect.any(String), + status: 'ok', + trace_id: expect.any(String), + }), + }), + ); +}); + +test('should create a document and trigger onDocumentCreated and another with authContext', async () => { + const serverTransactionPromise = waitForTransaction('node-firebase', span => { + return span.transaction === 'firebase.function.http.request'; + }); + + const serverTransactionOnDocumentCreatePromise = waitForTransaction('node-firebase', span => { + return ( + span.transaction === 'firebase.function.firestore.document.created' && + span.contexts?.trace?.data?.['faas.name'] === 'onDocumentCreate' + ); + }); + + const serverTransactionOnDocumentWithAuthContextCreatePromise = waitForTransaction('node-firebase', span => { + return ( + span.transaction === 'firebase.function.firestore.document.created' && + span.contexts?.trace?.data?.['faas.name'] === 'onDocumentCreateWithAuthContext' + ); + }); + + await fetch(`http://localhost:5001/demo-functions/default/onCallSomething`); + + const transactionEvent = await serverTransactionPromise; + const transactionEventOnDocumentCreate = await serverTransactionOnDocumentCreatePromise; + const transactionEventOnDocumentWithAuthContextCreate = await serverTransactionOnDocumentWithAuthContextCreatePromise; + + expect(transactionEvent.transaction).toEqual('firebase.function.http.request'); + expect(transactionEvent.contexts?.trace).toEqual({ + data: { + 'faas.name': 'onCallSomething', + 'faas.provider': 'firebase', + 'faas.trigger': 'http.request', + 'otel.kind': 'SERVER', + 'sentry.op': 'http.request', + 'sentry.origin': 'auto.firebase.otel.functions', + 'sentry.sample_rate': expect.any(Number), + 'sentry.source': 'route', + }, + op: 'http.request', + origin: 'auto.firebase.otel.functions', + span_id: expect.any(String), + status: 'ok', + trace_id: expect.any(String), + }); + expect(transactionEvent.spans).toHaveLength(3); + expect(transactionEventOnDocumentCreate.contexts?.trace).toEqual({ + data: { + 'faas.name': 'onDocumentCreate', + 'faas.provider': 'firebase', + 'faas.trigger': 'firestore.document.created', + 'otel.kind': 'SERVER', + 'sentry.op': expect.any(String), + 'sentry.origin': 'auto.firebase.otel.functions', + 'sentry.sample_rate': expect.any(Number), + 'sentry.source': 'route', + }, + op: expect.any(String), + origin: 'auto.firebase.otel.functions', + span_id: expect.any(String), + status: 'ok', + trace_id: expect.any(String), + }); + expect(transactionEventOnDocumentCreate.spans).toHaveLength(2); + expect(transactionEventOnDocumentWithAuthContextCreate.contexts?.trace).toEqual({ + data: { + 'faas.name': 'onDocumentCreateWithAuthContext', + 'faas.provider': 'firebase', + 'faas.trigger': 'firestore.document.created', + 'otel.kind': 'SERVER', + 'sentry.op': expect.any(String), + 'sentry.origin': 'auto.firebase.otel.functions', + 'sentry.sample_rate': expect.any(Number), + 'sentry.source': 'route', + }, + op: expect.any(String), + origin: 'auto.firebase.otel.functions', + span_id: expect.any(String), + status: 'ok', + trace_id: expect.any(String), + }); + expect(transactionEventOnDocumentWithAuthContextCreate.spans).toHaveLength(0); +}); diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/tsconfig.build.json b/dev-packages/e2e-tests/test-applications/node-firebase/tsconfig.build.json deleted file mode 100644 index 26c30d4eddf2..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-firebase/tsconfig.build.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "./tsconfig.json", - "exclude": ["node_modules", "test", "dist"] -} diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/tsconfig.json b/dev-packages/e2e-tests/test-applications/node-firebase/tsconfig.json index 8cb64e989ed9..881847032511 100644 --- a/dev-packages/e2e-tests/test-applications/node-firebase/tsconfig.json +++ b/dev-packages/e2e-tests/test-applications/node-firebase/tsconfig.json @@ -3,8 +3,6 @@ "types": ["node"], "esModuleInterop": true, "lib": ["es2018"], - "strict": true, - "outDir": "dist" - }, - "include": ["src/**/*.ts"] + "strict": true + } } From f201221fb908bd4f9585e96cc4ffb068977d147c Mon Sep 17 00:00:00 2001 From: JPeer264 Date: Mon, 20 Oct 2025 08:31:10 +0200 Subject: [PATCH 3/8] fixup! feat(firebase): update e2e tests to use functions --- .../node-firebase/firestore-app/package.json | 9 +-- .../node-firebase/functions/package.json | 4 +- .../node-firebase/functions/src/index.ts | 2 +- .../firebase/otel/patches/functions.ts | 56 +++++++------------ 4 files changed, 23 insertions(+), 48 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/firestore-app/package.json b/dev-packages/e2e-tests/test-applications/node-firebase/firestore-app/package.json index 434af16a99c2..b5d19993bdae 100644 --- a/dev-packages/e2e-tests/test-applications/node-firebase/firestore-app/package.json +++ b/dev-packages/e2e-tests/test-applications/node-firebase/firestore-app/package.json @@ -9,17 +9,12 @@ "dependencies": { "@firebase/app": "^0.13.1", "@sentry/node": "latest || *", - "@sentry/core": "latest || *", - "@sentry/opentelemetry": "latest || *", - "@types/node": "^22.13.14", - "dotenv": "^16.4.5", "express": "^4.18.2", - "firebase": "^12.0.0", - "firebase-admin": "^13.5.0", - "tsconfig-paths": "^4.2.0" + "firebase": "^12.0.0" }, "devDependencies": { "@types/express": "^4.17.13", + "@types/node": "^22.13.14", "typescript": "5.9.3" } } diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/functions/package.json b/dev-packages/e2e-tests/test-applications/node-firebase/functions/package.json index 178305b8c279..c3be318b8c38 100644 --- a/dev-packages/e2e-tests/test-applications/node-firebase/functions/package.json +++ b/dev-packages/e2e-tests/test-applications/node-firebase/functions/package.json @@ -11,9 +11,7 @@ "dependencies": { "firebase-admin": "^12.6.0", "firebase-functions": "^6.0.1", - "@sentry/node": "latest || *", - "@sentry/core": "latest || *", - "@sentry/opentelemetry": "latest || *" + "@sentry/node": "latest || *" }, "devDependencies": { "typescript": "5.9.3" diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/functions/src/index.ts b/dev-packages/e2e-tests/test-applications/node-firebase/functions/src/index.ts index a76dc6a0d2b1..16ad1bee3d4a 100644 --- a/dev-packages/e2e-tests/test-applications/node-firebase/functions/src/index.ts +++ b/dev-packages/e2e-tests/test-applications/node-firebase/functions/src/index.ts @@ -1,7 +1,7 @@ import './init'; import { onDocumentCreated, onDocumentCreatedWithAuthContext } from 'firebase-functions/firestore'; -import { onCall, onRequest } from 'firebase-functions/https'; +import { onRequest } from 'firebase-functions/https'; import * as logger from 'firebase-functions/logger'; import { setGlobalOptions } from 'firebase-functions/options'; import * as admin from 'firebase-admin'; diff --git a/packages/node/src/integrations/tracing/firebase/otel/patches/functions.ts b/packages/node/src/integrations/tracing/firebase/otel/patches/functions.ts index 4aed70ccd685..f4842b882ab2 100644 --- a/packages/node/src/integrations/tracing/firebase/otel/patches/functions.ts +++ b/packages/node/src/integrations/tracing/firebase/otel/patches/functions.ts @@ -66,42 +66,24 @@ export function patchFunctions( } const moduleFunctionsCJS = new InstrumentationNodeModuleDefinition('firebase-functions', functionsSupportedVersions); - - moduleFunctionsCJS.files.push( - new InstrumentationNodeModuleFile( - 'firebase-functions/lib/v2/providers/https.js', - functionsSupportedVersions, - moduleExports => wrapCommonFunctions(moduleExports, wrap, unwrap, tracer, functionsSpanCreationHook, 'function'), - moduleExports => unwrapCommonFunctions(moduleExports, unwrap), - ), - ); - - moduleFunctionsCJS.files.push( - new InstrumentationNodeModuleFile( - 'firebase-functions/lib/v2/providers/firestore.js', - functionsSupportedVersions, - moduleExports => wrapCommonFunctions(moduleExports, wrap, unwrap, tracer, functionsSpanCreationHook, 'firestore'), - moduleExports => unwrapCommonFunctions(moduleExports, unwrap), - ), - ); - - moduleFunctionsCJS.files.push( - new InstrumentationNodeModuleFile( - 'firebase-functions/lib/v2/providers/scheduler.js', - functionsSupportedVersions, - moduleExports => wrapCommonFunctions(moduleExports, wrap, unwrap, tracer, functionsSpanCreationHook, 'scheduler'), - moduleExports => unwrapCommonFunctions(moduleExports, unwrap), - ), - ); - - moduleFunctionsCJS.files.push( - new InstrumentationNodeModuleFile( - 'firebase-functions/lib/v2/storage.js', - functionsSupportedVersions, - moduleExports => wrapCommonFunctions(moduleExports, wrap, unwrap, tracer, functionsSpanCreationHook, 'storage'), - moduleExports => unwrapCommonFunctions(moduleExports, unwrap), - ), - ); + const modulesToInstrument = [ + { name: 'firebase-functions/lib/v2/providers/https.js', triggerType: 'function' }, + { name: 'firebase-functions/lib/v2/providers/firestore.js', triggerType: 'firestore' }, + { name: 'firebase-functions/lib/v2/providers/scheduler.js', triggerType: 'scheduler' }, + { name: 'firebase-functions/lib/v2/storage.js', triggerType: 'storage' }, + ] as const; + + modulesToInstrument.forEach(({ name, triggerType }) => { + moduleFunctionsCJS.files.push( + new InstrumentationNodeModuleFile( + name, + functionsSupportedVersions, + moduleExports => + wrapCommonFunctions(moduleExports, wrap, unwrap, tracer, functionsSpanCreationHook, triggerType), + moduleExports => unwrapCommonFunctions(moduleExports, unwrap), + ), + ); + }); return moduleFunctionsCJS; } @@ -190,7 +172,7 @@ export function patchV2Functions handler.apply(this, handlerArgs), err => { - if (err instanceof Error) { + if (err) { span.recordException(err); } From 4b8472e7c17e38428b7fbf8300a3927931750b9e Mon Sep 17 00:00:00 2001 From: JPeer264 Date: Tue, 21 Oct 2025 11:24:42 +0200 Subject: [PATCH 4/8] feat(firebase): add error handling and rename hooks to align with otel standards --- .../node-firebase/functions/src/index.ts | 4 + .../node-firebase/package.json | 3 +- .../node-firebase/tests/functions.test.ts | 35 +++- .../integrations/tracing/firebase/firebase.ts | 21 ++- .../firebase/otel/patches/functions.ts | 175 +++++++++++------- .../tracing/firebase/otel/types.ts | 14 +- 6 files changed, 172 insertions(+), 80 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/functions/src/index.ts b/dev-packages/e2e-tests/test-applications/node-firebase/functions/src/index.ts index 16ad1bee3d4a..6a3df6f4a61a 100644 --- a/dev-packages/e2e-tests/test-applications/node-firebase/functions/src/index.ts +++ b/dev-packages/e2e-tests/test-applications/node-firebase/functions/src/index.ts @@ -18,6 +18,10 @@ export const helloWorld = onRequest(async (request, response) => { response.send('Hello from Firebase!'); }); +export const unhandeledError = onRequest(async (request, response) => { + throw new Error('There is an error!'); +}); + export const onCallSomething = onRequest(async (request, response) => { const data = { name: request.body?.name || 'Sample Document', diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/package.json b/dev-packages/e2e-tests/test-applications/node-firebase/package.json index 13bf9e6df667..33c40ef32928 100644 --- a/dev-packages/e2e-tests/test-applications/node-firebase/package.json +++ b/dev-packages/e2e-tests/test-applications/node-firebase/package.json @@ -21,7 +21,8 @@ }, "devDependencies": { "@playwright/test": "~1.53.2", - "@sentry-internal/test-utils": "link:../../../test-utils" + "@sentry-internal/test-utils": "link:../../../test-utils", + "firebase": "^12.0.0" }, "volta": { "extends": "../../package.json" diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/tests/functions.test.ts b/dev-packages/e2e-tests/test-applications/node-firebase/tests/functions.test.ts index 19c92497c266..2600b8bc1ec5 100644 --- a/dev-packages/e2e-tests/test-applications/node-firebase/tests/functions.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-firebase/tests/functions.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; test('should only call the function once without any extra calls', async () => { const serverTransactionPromise = waitForTransaction('node-firebase', span => { @@ -15,6 +15,7 @@ test('should only call the function once without any extra calls', async () => { expect.objectContaining({ trace: expect.objectContaining({ data: { + 'cloud.project_id': 'demo-functions', 'faas.name': 'helloWorld', 'faas.provider': 'firebase', 'faas.trigger': 'http.request', @@ -34,6 +35,35 @@ test('should only call the function once without any extra calls', async () => { ); }); +test('should send failed transaction when the function fails', async () => { + const errorEventPromise = waitForError('node-firebase', () => true); + const serverTransactionPromise = waitForTransaction('node-firebase', span => { + return !!span.transaction; + }); + + await fetch(`http://localhost:5001/demo-functions/default/unhandeledError`); + + const transactionEvent = await serverTransactionPromise; + const errorEvent = await errorEventPromise; + + expect(transactionEvent.transaction).toEqual('firebase.function.http.request'); + expect(transactionEvent.contexts?.trace?.trace_id).toEqual(errorEvent.contexts?.trace?.trace_id); + expect(errorEvent).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'There is an error!', + mechanism: { + type: 'auto.firebase.otel.functions', + handled: false, + }, + }, + ], + }, + }); +}); + test('should create a document and trigger onDocumentCreated and another with authContext', async () => { const serverTransactionPromise = waitForTransaction('node-firebase', span => { return span.transaction === 'firebase.function.http.request'; @@ -62,6 +92,7 @@ test('should create a document and trigger onDocumentCreated and another with au expect(transactionEvent.transaction).toEqual('firebase.function.http.request'); expect(transactionEvent.contexts?.trace).toEqual({ data: { + 'cloud.project_id': 'demo-functions', 'faas.name': 'onCallSomething', 'faas.provider': 'firebase', 'faas.trigger': 'http.request', @@ -80,6 +111,7 @@ test('should create a document and trigger onDocumentCreated and another with au expect(transactionEvent.spans).toHaveLength(3); expect(transactionEventOnDocumentCreate.contexts?.trace).toEqual({ data: { + 'cloud.project_id': 'demo-functions', 'faas.name': 'onDocumentCreate', 'faas.provider': 'firebase', 'faas.trigger': 'firestore.document.created', @@ -98,6 +130,7 @@ test('should create a document and trigger onDocumentCreated and another with au expect(transactionEventOnDocumentCreate.spans).toHaveLength(2); expect(transactionEventOnDocumentWithAuthContextCreate.contexts?.trace).toEqual({ data: { + 'cloud.project_id': 'demo-functions', 'faas.name': 'onDocumentCreateWithAuthContext', 'faas.provider': 'firebase', 'faas.trigger': 'firestore.document.created', diff --git a/packages/node/src/integrations/tracing/firebase/firebase.ts b/packages/node/src/integrations/tracing/firebase/firebase.ts index 8429f3a15597..ceb521d54fa3 100644 --- a/packages/node/src/integrations/tracing/firebase/firebase.ts +++ b/packages/node/src/integrations/tracing/firebase/firebase.ts @@ -1,5 +1,5 @@ import type { IntegrationFn } from '@sentry/core'; -import { defineIntegration, SEMANTIC_ATTRIBUTE_SENTRY_OP } from '@sentry/core'; +import { captureException, defineIntegration, flush, SEMANTIC_ATTRIBUTE_SENTRY_OP } from '@sentry/core'; import { addOriginToSpan, generateInstrumentOnce } from '@sentry/node-core'; import { type FirebaseInstrumentationConfig, FirebaseInstrumentation } from './otel'; @@ -11,10 +11,23 @@ const config: FirebaseInstrumentationConfig = { span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'db.query'); }, - functionsSpanCreationHook: span => { - addOriginToSpan(span, 'auto.firebase.otel.functions'); + functions: { + requestHook: span => { + addOriginToSpan(span, 'auto.firebase.otel.functions'); - span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'http.request'); + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'http.request'); + }, + errorHook: async (_, error) => { + if (error) { + captureException(error, { + mechanism: { + type: 'auto.firebase.otel.functions', + handled: false, + }, + }); + await flush(2000); + } + }, }, }; diff --git a/packages/node/src/integrations/tracing/firebase/otel/patches/functions.ts b/packages/node/src/integrations/tracing/firebase/otel/patches/functions.ts index f4842b882ab2..9d84c1c42216 100644 --- a/packages/node/src/integrations/tracing/firebase/otel/patches/functions.ts +++ b/packages/node/src/integrations/tracing/firebase/otel/patches/functions.ts @@ -6,7 +6,6 @@ import { InstrumentationNodeModuleFile, isWrapped, safeExecuteInTheMiddle, - safeExecuteInTheMiddleAsync, } from '@opentelemetry/instrumentation'; import type { SpanAttributes } from '@sentry/core'; import type { @@ -28,7 +27,7 @@ import type { onObjectMetadataUpdated, } from 'firebase-functions/storage'; import type { FirebaseInstrumentation } from '../firebaseInstrumentation'; -import type { FirebaseInstrumentationConfig, FunctionsSpanCreationHook } from '../types'; +import type { FirebaseInstrumentationConfig, RequestHook, ResponseHook } from '../types'; /** * Patches Firebase Functions v2 to add OpenTelemetry instrumentation @@ -45,15 +44,30 @@ export function patchFunctions( unwrap: InstrumentationBase['_unwrap'], config: FirebaseInstrumentationConfig, ): InstrumentationNodeModuleDefinition { - const defaultFunctionsSpanCreationHook: FunctionsSpanCreationHook = () => {}; + let requestHook: RequestHook = () => {}; + let responseHook: ResponseHook = () => {}; + const errorHook = config.functions?.errorHook; + const configRequestHook = config.functions?.requestHook; + const configResponseHook = config.functions?.responseHook; - let functionsSpanCreationHook: FunctionsSpanCreationHook = defaultFunctionsSpanCreationHook; - const configFunctionsSpanCreationHook = config.functionsSpanCreationHook; - - if (typeof configFunctionsSpanCreationHook === 'function') { - functionsSpanCreationHook = (span: Span) => { + if (typeof configResponseHook === 'function') { + responseHook = (span: Span, err: unknown) => { safeExecuteInTheMiddle( - () => configFunctionsSpanCreationHook(span), + () => configResponseHook(span, err), + error => { + if (!error) { + return; + } + diag.error(error?.message); + }, + true, + ); + }; + } + if (typeof configRequestHook === 'function') { + requestHook = (span: Span) => { + safeExecuteInTheMiddle( + () => configRequestHook(span), error => { if (!error) { return; @@ -79,7 +93,14 @@ export function patchFunctions( name, functionsSupportedVersions, moduleExports => - wrapCommonFunctions(moduleExports, wrap, unwrap, tracer, functionsSpanCreationHook, triggerType), + wrapCommonFunctions( + moduleExports, + wrap, + unwrap, + tracer, + { requestHook, responseHook, errorHook }, + triggerType, + ), moduleExports => unwrapCommonFunctions(moduleExports, unwrap), ), ); @@ -123,17 +144,44 @@ type AvailableFirebaseFunctions = { type FirebaseFunctions = AvailableFirebaseFunctions[keyof AvailableFirebaseFunctions]; +/** + * Async function to execute patched function and being able to catch errors + * @param execute - function to be executed + * @param onFinish - callback to run when execute finishes + */ +export async function safeExecuteInTheMiddleAsync( + execute: () => T, + onFinish: (e: Error | undefined, result: T | undefined) => Promise | void, + preventThrowingError?: boolean, +): Promise { + let error: Error | undefined; + let result: T | undefined; + try { + result = await execute(); + } catch (e) { + error = e as Error; + } finally { + await onFinish?.(error, result); + if (error && !preventThrowingError) { + // eslint-disable-next-line no-unsafe-finally + throw error; + } + // eslint-disable-next-line no-unsafe-finally + return result as T; + } +} + /** * Patches Cloud Functions for Firebase (v2) to add OpenTelemetry instrumentation * * @param tracer - Opentelemetry Tracer - * @param functionsSpanCreationHook - Function to create a span for the function + * @param functionsConfig - Firebase instrumentation config * @param triggerType - Type of trigger * @returns A function that patches the function */ export function patchV2Functions( tracer: Tracer, - functionsSpanCreationHook: FunctionsSpanCreationHook, + functionsConfig: FirebaseInstrumentationConfig['functions'], triggerType: string, ): (original: T) => (...args: OverloadedParameters) => ReturnType { return function v2FunctionsWrapper(original: T): (...args: OverloadedParameters) => ReturnType { @@ -166,20 +214,35 @@ export function patchV2Functions - safeExecuteInTheMiddleAsync( - () => handler.apply(this, handlerArgs), - err => { - if (err) { - span.recordException(err); - } - - span.end(); - }, - ), - ); + functionsConfig?.requestHook?.(span); + + // Can be changed to safeExecuteInTheMiddleAsync once following is merged and released + // https://github.com/open-telemetry/opentelemetry-js/pull/6032 + return context.with(trace.setSpan(context.active(), span), async () => { + let error: Error | undefined; + let result: T | undefined; + + try { + result = await handler.apply(this, handlerArgs); + } catch (e) { + error = e as Error; + } + + functionsConfig?.responseHook?.(span, error); + + if (error) { + span.recordException(error); + } + + span.end(); + + if (error) { + await functionsConfig?.errorHook?.(span, error); + throw error; + } + + return result; + }); }; if (documentOrOptions) { @@ -196,86 +259,58 @@ function wrapCommonFunctions( wrap: InstrumentationBase['_wrap'], unwrap: InstrumentationBase['_unwrap'], tracer: Tracer, - functionsSpanCreationHook: FunctionsSpanCreationHook, + functionsConfig: FirebaseInstrumentationConfig['functions'], triggerType: 'function' | 'firestore' | 'scheduler' | 'storage', ): AvailableFirebaseFunctions { unwrapCommonFunctions(moduleExports, unwrap); switch (triggerType) { case 'function': - wrap(moduleExports, 'onRequest', patchV2Functions(tracer, functionsSpanCreationHook, 'http.request')); - wrap(moduleExports, 'onCall', patchV2Functions(tracer, functionsSpanCreationHook, 'http.call')); + wrap(moduleExports, 'onRequest', patchV2Functions(tracer, functionsConfig, 'http.request')); + wrap(moduleExports, 'onCall', patchV2Functions(tracer, functionsConfig, 'http.call')); break; case 'firestore': - wrap( - moduleExports, - 'onDocumentCreated', - patchV2Functions(tracer, functionsSpanCreationHook, 'firestore.document.created'), - ); - wrap( - moduleExports, - 'onDocumentUpdated', - patchV2Functions(tracer, functionsSpanCreationHook, 'firestore.document.updated'), - ); - wrap( - moduleExports, - 'onDocumentDeleted', - patchV2Functions(tracer, functionsSpanCreationHook, 'firestore.document.deleted'), - ); - wrap( - moduleExports, - 'onDocumentWritten', - patchV2Functions(tracer, functionsSpanCreationHook, 'firestore.document.written'), - ); + wrap(moduleExports, 'onDocumentCreated', patchV2Functions(tracer, functionsConfig, 'firestore.document.created')); + wrap(moduleExports, 'onDocumentUpdated', patchV2Functions(tracer, functionsConfig, 'firestore.document.updated')); + wrap(moduleExports, 'onDocumentDeleted', patchV2Functions(tracer, functionsConfig, 'firestore.document.deleted')); + wrap(moduleExports, 'onDocumentWritten', patchV2Functions(tracer, functionsConfig, 'firestore.document.written')); wrap( moduleExports, 'onDocumentCreatedWithAuthContext', - patchV2Functions(tracer, functionsSpanCreationHook, 'firestore.document.created'), + patchV2Functions(tracer, functionsConfig, 'firestore.document.created'), ); wrap( moduleExports, 'onDocumentUpdatedWithAuthContext', - patchV2Functions(tracer, functionsSpanCreationHook, 'firestore.document.updated'), + patchV2Functions(tracer, functionsConfig, 'firestore.document.updated'), ); wrap( moduleExports, 'onDocumentDeletedWithAuthContext', - patchV2Functions(tracer, functionsSpanCreationHook, 'firestore.document.deleted'), + patchV2Functions(tracer, functionsConfig, 'firestore.document.deleted'), ); wrap( moduleExports, 'onDocumentWrittenWithAuthContext', - patchV2Functions(tracer, functionsSpanCreationHook, 'firestore.document.written'), + patchV2Functions(tracer, functionsConfig, 'firestore.document.written'), ); break; case 'scheduler': - wrap(moduleExports, 'onSchedule', patchV2Functions(tracer, functionsSpanCreationHook, 'scheduler.scheduled')); + wrap(moduleExports, 'onSchedule', patchV2Functions(tracer, functionsConfig, 'scheduler.scheduled')); break; case 'storage': - wrap( - moduleExports, - 'onObjectFinalized', - patchV2Functions(tracer, functionsSpanCreationHook, 'storage.object.finalized'), - ); - wrap( - moduleExports, - 'onObjectArchived', - patchV2Functions(tracer, functionsSpanCreationHook, 'storage.object.archived'), - ); - wrap( - moduleExports, - 'onObjectDeleted', - patchV2Functions(tracer, functionsSpanCreationHook, 'storage.object.deleted'), - ); + wrap(moduleExports, 'onObjectFinalized', patchV2Functions(tracer, functionsConfig, 'storage.object.finalized')); + wrap(moduleExports, 'onObjectArchived', patchV2Functions(tracer, functionsConfig, 'storage.object.archived')); + wrap(moduleExports, 'onObjectDeleted', patchV2Functions(tracer, functionsConfig, 'storage.object.deleted')); wrap( moduleExports, 'onObjectMetadataUpdated', - patchV2Functions(tracer, functionsSpanCreationHook, 'storage.object.metadataUpdated'), + patchV2Functions(tracer, functionsConfig, 'storage.object.metadataUpdated'), ); break; } diff --git a/packages/node/src/integrations/tracing/firebase/otel/types.ts b/packages/node/src/integrations/tracing/firebase/otel/types.ts index aa48d4c8fec2..dca5b4c5b223 100644 --- a/packages/node/src/integrations/tracing/firebase/otel/types.ts +++ b/packages/node/src/integrations/tracing/firebase/otel/types.ts @@ -88,14 +88,20 @@ export interface FirestoreSettings { */ export interface FirebaseInstrumentationConfig extends InstrumentationConfig { firestoreSpanCreationHook?: FirestoreSpanCreationHook; - functionsSpanCreationHook?: FunctionsSpanCreationHook; + functions?: FunctionsConfig; } -export interface FirestoreSpanCreationHook { - (span: Span): void; +export interface FunctionsConfig { + requestHook?: RequestHook; + responseHook?: ResponseHook; + errorHook?: ErrorHook; } -export interface FunctionsSpanCreationHook { +export type RequestHook = (span: Span) => void; +export type ResponseHook = (span: Span, error?: unknown) => void; +export type ErrorHook = (span: Span, error?: unknown) => Promise | void; + +export interface FirestoreSpanCreationHook { (span: Span): void; } From bc71a9856ebc2d90173e5835bc94dcb5fca4f935 Mon Sep 17 00:00:00 2001 From: JPeer264 Date: Tue, 21 Oct 2025 11:57:41 +0200 Subject: [PATCH 5/8] feat(firebase): remove firebase-functions as dependency --- packages/node/package.json | 1 - .../firebase/otel/patches/functions.ts | 62 ++--------- .../tracing/firebase/otel/types.ts | 37 ++++++ yarn.lock | 105 ------------------ 4 files changed, 45 insertions(+), 160 deletions(-) diff --git a/packages/node/package.json b/packages/node/package.json index 2d73d82405df..a78a670a6cdd 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -98,7 +98,6 @@ "@sentry/core": "10.20.0", "@sentry/node-core": "10.20.0", "@sentry/opentelemetry": "10.20.0", - "firebase-functions": "^6.5.0", "import-in-the-middle": "^1.14.2", "minimatch": "^9.0.0" }, diff --git a/packages/node/src/integrations/tracing/firebase/otel/patches/functions.ts b/packages/node/src/integrations/tracing/firebase/otel/patches/functions.ts index 9d84c1c42216..1f182e325e67 100644 --- a/packages/node/src/integrations/tracing/firebase/otel/patches/functions.ts +++ b/packages/node/src/integrations/tracing/firebase/otel/patches/functions.ts @@ -8,26 +8,15 @@ import { safeExecuteInTheMiddle, } from '@opentelemetry/instrumentation'; import type { SpanAttributes } from '@sentry/core'; -import type { - onDocumentCreated, - onDocumentCreatedWithAuthContext, - onDocumentDeleted, - onDocumentDeletedWithAuthContext, - onDocumentUpdated, - onDocumentUpdatedWithAuthContext, - onDocumentWritten, - onDocumentWrittenWithAuthContext, -} from 'firebase-functions/firestore'; -import type { onCall, onRequest } from 'firebase-functions/https'; -import type { onSchedule } from 'firebase-functions/scheduler'; -import type { - onObjectArchived, - onObjectDeleted, - onObjectFinalized, - onObjectMetadataUpdated, -} from 'firebase-functions/storage'; import type { FirebaseInstrumentation } from '../firebaseInstrumentation'; -import type { FirebaseInstrumentationConfig, RequestHook, ResponseHook } from '../types'; +import type { + AvailableFirebaseFunctions, + FirebaseFunctions, + FirebaseInstrumentationConfig, + OverloadedParameters, + RequestHook, + ResponseHook, +} from '../types'; /** * Patches Firebase Functions v2 to add OpenTelemetry instrumentation @@ -109,41 +98,6 @@ export function patchFunctions( return moduleFunctionsCJS; } -type OverloadedParameters = T extends { - (...args: infer A1): unknown; - (...args: infer A2): unknown; - (...args: infer A3): unknown; - (...args: infer A4): unknown; -} - ? A1 | A2 | A3 | A4 - : T extends { (...args: infer A1): unknown; (...args: infer A2): unknown; (...args: infer A3): unknown } - ? A1 | A2 | A3 - : T extends { (...args: infer A1): unknown; (...args: infer A2): unknown } - ? A1 | A2 - : T extends (...args: infer A) => unknown - ? A - : unknown; - -type AvailableFirebaseFunctions = { - onRequest: typeof onRequest; - onCall: typeof onCall; - onDocumentCreated: typeof onDocumentCreated; - onDocumentUpdated: typeof onDocumentUpdated; - onDocumentDeleted: typeof onDocumentDeleted; - onDocumentWritten: typeof onDocumentWritten; - onDocumentCreatedWithAuthContext: typeof onDocumentCreatedWithAuthContext; - onDocumentUpdatedWithAuthContext: typeof onDocumentUpdatedWithAuthContext; - onDocumentDeletedWithAuthContext: typeof onDocumentDeletedWithAuthContext; - onDocumentWrittenWithAuthContext: typeof onDocumentWrittenWithAuthContext; - onSchedule: typeof onSchedule; - onObjectFinalized: typeof onObjectFinalized; - onObjectArchived: typeof onObjectArchived; - onObjectDeleted: typeof onObjectDeleted; - onObjectMetadataUpdated: typeof onObjectMetadataUpdated; -}; - -type FirebaseFunctions = AvailableFirebaseFunctions[keyof AvailableFirebaseFunctions]; - /** * Async function to execute patched function and being able to catch errors * @param execute - function to be executed diff --git a/packages/node/src/integrations/tracing/firebase/otel/types.ts b/packages/node/src/integrations/tracing/firebase/otel/types.ts index dca5b4c5b223..ead830fa2c1a 100644 --- a/packages/node/src/integrations/tracing/firebase/otel/types.ts +++ b/packages/node/src/integrations/tracing/firebase/otel/types.ts @@ -128,3 +128,40 @@ export type AddDocType = ( export type DeleteDocType = ( reference: DocumentReference, ) => Promise; + +export type OverloadedParameters = T extends { + (...args: infer A1): unknown; + (...args: infer A2): unknown; +} + ? A1 | A2 + : T extends (...args: infer A) => unknown + ? A + : unknown; + +/** + * A bare minimum of how Cloud Functions for Firebase (v2) are defined. + */ +export type FirebaseFunctions = + | ((handler: () => Promise | unknown) => (...args: unknown[]) => Promise | unknown) + | (( + documentOrOptions: string | string[] | Record, + handler: () => Promise | unknown, + ) => (...args: unknown[]) => Promise | unknown); + +export type AvailableFirebaseFunctions = { + onRequest: FirebaseFunctions; + onCall: FirebaseFunctions; + onDocumentCreated: FirebaseFunctions; + onDocumentUpdated: FirebaseFunctions; + onDocumentDeleted: FirebaseFunctions; + onDocumentWritten: FirebaseFunctions; + onDocumentCreatedWithAuthContext: FirebaseFunctions; + onDocumentUpdatedWithAuthContext: FirebaseFunctions; + onDocumentDeletedWithAuthContext: FirebaseFunctions; + onDocumentWrittenWithAuthContext: FirebaseFunctions; + onSchedule: FirebaseFunctions; + onObjectFinalized: FirebaseFunctions; + onObjectArchived: FirebaseFunctions; + onObjectDeleted: FirebaseFunctions; + onObjectMetadataUpdated: FirebaseFunctions; +}; diff --git a/yarn.lock b/yarn.lock index a2562cbe0cfa..70c9e3d80b73 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8272,13 +8272,6 @@ resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.12.tgz#6b2c510a7ad7039e98e7b8d3d6598f4359e5c080" integrity sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw== -"@types/cors@^2.8.5": - version "2.8.19" - resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.19.tgz#d93ea2673fd8c9f697367f5eeefc2bbfa94f0342" - integrity sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg== - dependencies: - "@types/node" "*" - "@types/css-font-loading-module@0.0.7": version "0.0.7" resolved "https://registry.yarnpkg.com/@types/css-font-loading-module/-/css-font-loading-module-0.0.7.tgz#2f98ede46acc0975de85c0b7b0ebe06041d24601" @@ -8541,16 +8534,6 @@ "@types/qs" "*" "@types/serve-static" "*" -"@types/express@^4.17.21": - version "4.17.23" - resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.23.tgz#35af3193c640bfd4d7fe77191cd0ed411a433bef" - integrity sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ== - dependencies: - "@types/body-parser" "*" - "@types/express-serve-static-core" "^4.17.33" - "@types/qs" "*" - "@types/serve-static" "*" - "@types/fs-extra@^5.0.5": version "5.1.0" resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-5.1.0.tgz#2a325ef97901504a3828718c390d34b8426a10a1" @@ -8797,13 +8780,6 @@ dependencies: undici-types "~6.20.0" -"@types/node@>=13.7.0": - version "24.7.2" - resolved "https://registry.yarnpkg.com/@types/node/-/node-24.7.2.tgz#5adf66b6e2ac5cab1d10a2ad3682e359cb652f4a" - integrity sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA== - dependencies: - undici-types "~7.14.0" - "@types/node@^10.1.0": version "10.17.60" resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.60.tgz#35f3d6213daed95da7f0f73e75bcc6980e90597b" @@ -16775,43 +16751,6 @@ express@^4.10.7, express@^4.17.1, express@^4.17.3, express@^4.18.1, express@^4.2 utils-merge "1.0.1" vary "~1.1.2" -express@^4.21.0: - version "4.21.2" - resolved "https://registry.yarnpkg.com/express/-/express-4.21.2.tgz#cf250e48362174ead6cea4a566abef0162c1ec32" - integrity sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA== - dependencies: - accepts "~1.3.8" - array-flatten "1.1.1" - body-parser "1.20.3" - content-disposition "0.5.4" - content-type "~1.0.4" - cookie "0.7.1" - cookie-signature "1.0.6" - debug "2.6.9" - depd "2.0.0" - encodeurl "~2.0.0" - escape-html "~1.0.3" - etag "~1.8.1" - finalhandler "1.3.1" - fresh "0.5.2" - http-errors "2.0.0" - merge-descriptors "1.0.3" - methods "~1.1.2" - on-finished "2.4.1" - parseurl "~1.3.3" - path-to-regexp "0.1.12" - proxy-addr "~2.0.7" - qs "6.13.0" - range-parser "~1.2.1" - safe-buffer "5.2.1" - send "0.19.0" - serve-static "1.16.2" - setprototypeof "1.2.0" - statuses "2.0.1" - type-is "~1.6.18" - utils-merge "1.0.1" - vary "~1.1.2" - exsolve@^1.0.4, exsolve@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/exsolve/-/exsolve-1.0.7.tgz#3b74e4c7ca5c5f9a19c3626ca857309fa99f9e9e" @@ -17339,17 +17278,6 @@ findup-sync@^4.0.0: micromatch "^4.0.2" resolve-dir "^1.0.1" -firebase-functions@^6.5.0: - version "6.5.0" - resolved "https://registry.yarnpkg.com/firebase-functions/-/firebase-functions-6.5.0.tgz#0324babc47afbc4e10a3a5b9b289bd0f763e6ff8" - integrity sha512-ffntJkq88K0pdLDq14IyetKQu99Je4vBBJBe9rkZK2X4QyeJJnmA527+v2iTKA+SwRtxf5lh7sXgxpvxGS8HtQ== - dependencies: - "@types/cors" "^2.8.5" - "@types/express" "^4.17.21" - cors "^2.8.5" - express "^4.21.0" - protobufjs "^7.2.2" - fireworm@^0.7.2: version "0.7.2" resolved "https://registry.yarnpkg.com/fireworm/-/fireworm-0.7.2.tgz#bc5736515b48bd30bf3293a2062e0b0e0361537a" @@ -21402,11 +21330,6 @@ long@^4.0.0: resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== -long@^5.0.0: - version "5.3.2" - resolved "https://registry.yarnpkg.com/long/-/long-5.3.2.tgz#1d84463095999262d7d7b7f8bfd4a8cc55167f83" - integrity sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA== - long@^5.2.1: version "5.2.3" resolved "https://registry.yarnpkg.com/long/-/long-5.2.3.tgz#a3ba97f3877cf1d778eccbcb048525ebb77499e1" @@ -24829,11 +24752,6 @@ path-to-regexp@0.1.10: resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.10.tgz#67e9108c5c0551b9e5326064387de4763c4d5f8b" integrity sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w== -path-to-regexp@0.1.12: - version "0.1.12" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz#d5e1a12e478a976d432ef3c58d534b9923164bb7" - integrity sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ== - path-to-regexp@3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-3.3.0.tgz#f7f31d32e8518c2660862b644414b6d5c63a611b" @@ -26113,24 +26031,6 @@ property-information@^6.0.0: resolved "https://registry.yarnpkg.com/property-information/-/property-information-6.3.0.tgz#ba4a06ec6b4e1e90577df9931286953cdf4282c3" integrity sha512-gVNZ74nqhRMiIUYWGQdosYetaKc83x8oT41a0LlV3AAFCAZwCpg4vmGkq8t34+cUhp3cnM4XDiU/7xlgK7HGrg== -protobufjs@^7.2.2: - version "7.5.4" - resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.5.4.tgz#885d31fe9c4b37f25d1bb600da30b1c5b37d286a" - integrity sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg== - dependencies: - "@protobufjs/aspromise" "^1.1.2" - "@protobufjs/base64" "^1.1.2" - "@protobufjs/codegen" "^2.0.4" - "@protobufjs/eventemitter" "^1.1.0" - "@protobufjs/fetch" "^1.1.0" - "@protobufjs/float" "^1.0.2" - "@protobufjs/inquire" "^1.1.0" - "@protobufjs/path" "^1.1.2" - "@protobufjs/pool" "^1.1.0" - "@protobufjs/utf8" "^1.1.0" - "@types/node" ">=13.7.0" - long "^5.0.0" - protocols@^2.0.0, protocols@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/protocols/-/protocols-2.0.1.tgz#8f155da3fc0f32644e83c5782c8e8212ccf70a86" @@ -30304,11 +30204,6 @@ undici-types@~6.20.0: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433" integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg== -undici-types@~7.14.0: - version "7.14.0" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.14.0.tgz#4c037b32ca4d7d62fae042174604341588bc0840" - integrity sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA== - undici@^5.25.4, undici@^5.28.5: version "5.29.0" resolved "https://registry.yarnpkg.com/undici/-/undici-5.29.0.tgz#419595449ae3f2cdcba3580a2e8903399bd1f5a3" From c2cb176710abd6e4577fccde45cc3acea90722a6 Mon Sep 17 00:00:00 2001 From: JPeer264 Date: Wed, 22 Oct 2025 13:41:38 +0200 Subject: [PATCH 6/8] fixup! feat(firebase): update e2e tests to use functions --- .../e2e-tests/test-applications/node-firebase/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/package.json b/dev-packages/e2e-tests/test-applications/node-firebase/package.json index 33c40ef32928..41eb0ce085d4 100644 --- a/dev-packages/e2e-tests/test-applications/node-firebase/package.json +++ b/dev-packages/e2e-tests/test-applications/node-firebase/package.json @@ -22,7 +22,7 @@ "devDependencies": { "@playwright/test": "~1.53.2", "@sentry-internal/test-utils": "link:../../../test-utils", - "firebase": "^12.0.0" + "firebase-tools": "^14.20.0" }, "volta": { "extends": "../../package.json" From a431a545d8f1cd2056e088176689b39a1e23c880 Mon Sep 17 00:00:00 2001 From: JPeer264 Date: Wed, 22 Oct 2025 13:47:10 +0200 Subject: [PATCH 7/8] fixup! feat(firebase): add error handling and rename hooks to align with otel standards --- .../firebase/otel/patches/functions.ts | 27 ------------------- 1 file changed, 27 deletions(-) diff --git a/packages/node/src/integrations/tracing/firebase/otel/patches/functions.ts b/packages/node/src/integrations/tracing/firebase/otel/patches/functions.ts index 1f182e325e67..a3ef7546b603 100644 --- a/packages/node/src/integrations/tracing/firebase/otel/patches/functions.ts +++ b/packages/node/src/integrations/tracing/firebase/otel/patches/functions.ts @@ -98,33 +98,6 @@ export function patchFunctions( return moduleFunctionsCJS; } -/** - * Async function to execute patched function and being able to catch errors - * @param execute - function to be executed - * @param onFinish - callback to run when execute finishes - */ -export async function safeExecuteInTheMiddleAsync( - execute: () => T, - onFinish: (e: Error | undefined, result: T | undefined) => Promise | void, - preventThrowingError?: boolean, -): Promise { - let error: Error | undefined; - let result: T | undefined; - try { - result = await execute(); - } catch (e) { - error = e as Error; - } finally { - await onFinish?.(error, result); - if (error && !preventThrowingError) { - // eslint-disable-next-line no-unsafe-finally - throw error; - } - // eslint-disable-next-line no-unsafe-finally - return result as T; - } -} - /** * Patches Cloud Functions for Firebase (v2) to add OpenTelemetry instrumentation * From e532f44d40f535afb54f6af247b2e65949fbf3c5 Mon Sep 17 00:00:00 2001 From: JPeer264 Date: Thu, 23 Oct 2025 11:06:51 +0200 Subject: [PATCH 8/8] feat: Update sdk size --- .size-limit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.size-limit.js b/.size-limit.js index 269ce49b1cc1..7106f2e29b03 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -240,7 +240,7 @@ module.exports = [ import: createImport('init'), ignore: [...builtinModules, ...nodePrefixedBuiltinModules], gzip: true, - limit: '157 KB', + limit: '158 KB', }, { name: '@sentry/node - without tracing',