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',
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
+
+
+
+
+
-### 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..b5d19993bdae
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/node-firebase/firestore-app/package.json
@@ -0,0 +1,20 @@
+{
+ "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 || *",
+ "express": "^4.18.2",
+ "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/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..c3be318b8c38
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/node-firebase/functions/package.json
@@ -0,0 +1,19 @@
+{
+ "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 || *"
+ },
+ "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..6a3df6f4a61a
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/node-firebase/functions/src/index.ts
@@ -0,0 +1,50 @@
+import './init';
+
+import { onDocumentCreated, onDocumentCreatedWithAuthContext } from 'firebase-functions/firestore';
+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';
+
+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 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',
+ 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..41eb0ce085d4 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,26 @@
"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"
+ "firebase-tools": "^14.20.0"
},
"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..2600b8bc1ec5
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/node-firebase/tests/functions.test.ts
@@ -0,0 +1,150 @@
+import { expect, test } from '@playwright/test';
+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 => {
+ 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: {
+ 'cloud.project_id': 'demo-functions',
+ '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 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';
+ });
+
+ 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: {
+ 'cloud.project_id': 'demo-functions',
+ '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: {
+ 'cloud.project_id': 'demo-functions',
+ '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: {
+ 'cloud.project_id': 'demo-functions',
+ '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
+ }
}
diff --git a/packages/node/src/integrations/tracing/firebase/firebase.ts b/packages/node/src/integrations/tracing/firebase/firebase.ts
index 649a7089289b..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,6 +11,24 @@ const config: FirebaseInstrumentationConfig = {
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'db.query');
},
+ functions: {
+ requestHook: span => {
+ addOriginToSpan(span, 'auto.firebase.otel.functions');
+
+ 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);
+ }
+ },
+ },
};
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 responseHook: ResponseHook = () => {};
+ const errorHook = config.functions?.errorHook;
+ const configRequestHook = config.functions?.requestHook;
+ const configResponseHook = config.functions?.responseHook;
+
+ if (typeof configResponseHook === 'function') {
+ responseHook = (span: Span, err: unknown) => {
+ safeExecuteInTheMiddle(
+ () => 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;
+ }
+ diag.error(error?.message);
+ },
+ true,
+ );
+ };
+ }
+
+ const moduleFunctionsCJS = new InstrumentationNodeModuleDefinition('firebase-functions', functionsSupportedVersions);
+ 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,
+ { requestHook, responseHook, errorHook },
+ triggerType,
+ ),
+ moduleExports => unwrapCommonFunctions(moduleExports, unwrap),
+ ),
+ );
+ });
+
+ return moduleFunctionsCJS;
+}
+
+/**
+ * Patches Cloud Functions for Firebase (v2) to add OpenTelemetry instrumentation
+ *
+ * @param tracer - Opentelemetry Tracer
+ * @param functionsConfig - Firebase instrumentation config
+ * @param triggerType - Type of trigger
+ * @returns A function that patches the function
+ */
+export function patchV2Functions(
+ tracer: Tracer,
+ functionsConfig: FirebaseInstrumentationConfig['functions'],
+ 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);
+ 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) {
+ return original.call(this, documentOrOptions, wrappedHandler);
+ } else {
+ return original.call(this, wrappedHandler);
+ }
+ };
+ };
+}
+
+function wrapCommonFunctions(
+ moduleExports: AvailableFirebaseFunctions,
+ wrap: InstrumentationBase['_wrap'],
+ unwrap: InstrumentationBase['_unwrap'],
+ tracer: Tracer,
+ functionsConfig: FirebaseInstrumentationConfig['functions'],
+ triggerType: 'function' | 'firestore' | 'scheduler' | 'storage',
+): AvailableFirebaseFunctions {
+ unwrapCommonFunctions(moduleExports, unwrap);
+
+ switch (triggerType) {
+ case 'function':
+ wrap(moduleExports, 'onRequest', patchV2Functions(tracer, functionsConfig, 'http.request'));
+ wrap(moduleExports, 'onCall', patchV2Functions(tracer, functionsConfig, 'http.call'));
+ break;
+
+ case 'firestore':
+ 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, functionsConfig, 'firestore.document.created'),
+ );
+ wrap(
+ moduleExports,
+ 'onDocumentUpdatedWithAuthContext',
+ patchV2Functions(tracer, functionsConfig, 'firestore.document.updated'),
+ );
+
+ wrap(
+ moduleExports,
+ 'onDocumentDeletedWithAuthContext',
+ patchV2Functions(tracer, functionsConfig, 'firestore.document.deleted'),
+ );
+
+ wrap(
+ moduleExports,
+ 'onDocumentWrittenWithAuthContext',
+ patchV2Functions(tracer, functionsConfig, 'firestore.document.written'),
+ );
+ break;
+
+ case 'scheduler':
+ wrap(moduleExports, 'onSchedule', patchV2Functions(tracer, functionsConfig, 'scheduler.scheduled'));
+ break;
+
+ case 'storage':
+ 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, functionsConfig, '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..ead830fa2c1a 100644
--- a/packages/node/src/integrations/tracing/firebase/otel/types.ts
+++ b/packages/node/src/integrations/tracing/firebase/otel/types.ts
@@ -88,8 +88,19 @@ export interface FirestoreSettings {
*/
export interface FirebaseInstrumentationConfig extends InstrumentationConfig {
firestoreSpanCreationHook?: FirestoreSpanCreationHook;
+ functions?: FunctionsConfig;
}
+export interface FunctionsConfig {
+ requestHook?: RequestHook;
+ responseHook?: ResponseHook;
+ errorHook?: ErrorHook;
+}
+
+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;
}
@@ -117,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;
+};