From 31381d54cdf6cf7c73242adec5963eac00fea139 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Tue, 24 Jun 2025 15:03:22 +0100 Subject: [PATCH 01/20] feat(node): Add firebase integration --- .../node-firebase/.gitignore | 58 ++++ .../test-applications/node-firebase/.npmrc | 2 + .../test-applications/node-firebase/README.md | 64 ++++ .../node-firebase/createConfigFromEnv.js | 14 + .../node-firebase/createEnvFromConfig.js | 10 + .../node-firebase/docker/.gitignore | 6 + .../node-firebase/docker/Dockerfile | 15 + .../node-firebase/docker/docker-compose.yml | 60 ++++ .../node-firebase/docker/firebase/.firebaserc | 5 + .../node-firebase/docker/firebase/.gitignore | 69 +++++ .../createOrUpdateConfigIfPossible.js | 3 + .../docker/firebase/database.rules.json | 7 + .../docker/firebase/firebase.json | 25 ++ .../docker/firebase/firestore.indexes.json | 4 + .../docker/firebase/firestore.rules | 8 + .../docker/firebase/storage.rules | 8 + .../node-firebase/docker/firebase/utils.js | 145 +++++++++ .../node-firebase/docker/nginx.conf | 150 +++++++++ .../node-firebase/docker/serve.sh | 64 ++++ .../node-firebase/package.json | 34 ++ .../node-firebase/playwright.config.mjs | 7 + .../node-firebase/src/app.ts | 71 +++++ .../node-firebase/src/init.ts | 11 + .../node-firebase/start-event-proxy.mjs | 6 + .../node-firebase/tests/transactions.test.ts | 117 +++++++ .../node-firebase/tsconfig.build.json | 4 + .../node-firebase/tsconfig.json | 10 + packages/node/src/index.ts | 1 + .../integrations/tracing/firebase/README.md | 1 + .../integrations/tracing/firebase/firebase.ts | 29 ++ .../integrations/tracing/firebase/index.ts | 1 + .../firebase/otel/firebaseInstrumentation.ts | 37 +++ .../tracing/firebase/otel/index.ts | 2 + .../firebase/otel/patches/firestore.ts | 291 ++++++++++++++++++ .../tracing/firebase/otel/types.ts | 119 +++++++ .../node/src/integrations/tracing/index.ts | 3 + 36 files changed, 1461 insertions(+) create mode 100644 dev-packages/e2e-tests/test-applications/node-firebase/.gitignore create mode 100644 dev-packages/e2e-tests/test-applications/node-firebase/.npmrc create mode 100644 dev-packages/e2e-tests/test-applications/node-firebase/README.md create mode 100644 dev-packages/e2e-tests/test-applications/node-firebase/createConfigFromEnv.js create mode 100644 dev-packages/e2e-tests/test-applications/node-firebase/createEnvFromConfig.js create mode 100644 dev-packages/e2e-tests/test-applications/node-firebase/docker/.gitignore create mode 100644 dev-packages/e2e-tests/test-applications/node-firebase/docker/Dockerfile create mode 100644 dev-packages/e2e-tests/test-applications/node-firebase/docker/docker-compose.yml create mode 100644 dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/.firebaserc create mode 100644 dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/.gitignore create mode 100644 dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/createOrUpdateConfigIfPossible.js create mode 100644 dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/database.rules.json create mode 100644 dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/firebase.json create mode 100644 dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/firestore.indexes.json create mode 100644 dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/firestore.rules create mode 100644 dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/storage.rules create mode 100644 dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/utils.js create mode 100644 dev-packages/e2e-tests/test-applications/node-firebase/docker/nginx.conf create mode 100644 dev-packages/e2e-tests/test-applications/node-firebase/docker/serve.sh create mode 100644 dev-packages/e2e-tests/test-applications/node-firebase/package.json create mode 100644 dev-packages/e2e-tests/test-applications/node-firebase/playwright.config.mjs create mode 100644 dev-packages/e2e-tests/test-applications/node-firebase/src/app.ts create mode 100644 dev-packages/e2e-tests/test-applications/node-firebase/src/init.ts create mode 100644 dev-packages/e2e-tests/test-applications/node-firebase/start-event-proxy.mjs create mode 100644 dev-packages/e2e-tests/test-applications/node-firebase/tests/transactions.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/node-firebase/tsconfig.build.json create mode 100644 dev-packages/e2e-tests/test-applications/node-firebase/tsconfig.json create mode 100644 packages/node/src/integrations/tracing/firebase/README.md create mode 100644 packages/node/src/integrations/tracing/firebase/firebase.ts create mode 100644 packages/node/src/integrations/tracing/firebase/index.ts create mode 100644 packages/node/src/integrations/tracing/firebase/otel/firebaseInstrumentation.ts create mode 100644 packages/node/src/integrations/tracing/firebase/otel/index.ts create mode 100644 packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts create mode 100644 packages/node/src/integrations/tracing/firebase/otel/types.ts diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/.gitignore b/dev-packages/e2e-tests/test-applications/node-firebase/.gitignore new file mode 100644 index 000000000000..48b1bd712db4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-firebase/.gitignore @@ -0,0 +1,58 @@ +# compiled output +/dist +/node_modules +/build + +# Logs +logs +*.log +npm-debug.log* +pnpm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# OS +.DS_Store + +# Tests +/coverage +/.nyc_output + +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# IDE - VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# temp directory +.temp +.tmp + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +test-results diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/.npmrc b/dev-packages/e2e-tests/test-applications/node-firebase/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-firebase/.npmrc @@ -0,0 +1,2 @@ +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/README.md b/dev-packages/e2e-tests/test-applications/node-firebase/README.md new file mode 100644 index 000000000000..e44ee12f5268 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-firebase/README.md @@ -0,0 +1,64 @@ +## Assuming you already have installed docker desktop or orbstack etc. or any other docker software + +### Enabling / authorising firebase emulator through docker + +1. Run the docker + +```bash +pnpm docker +``` + +2. In new tab, enter the docker container by simply running + +```bash +docker exec -it sentry-firebase bash +``` + +3. Now inside docker container run + +```bash +firebase login +``` + +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 +``` + +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 + +### 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 +``` + +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. + +**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.** + +### Scripts - helpers + +- 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 + +Use these scripts when testing and updating the environment settings on CLI diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/createConfigFromEnv.js b/dev-packages/e2e-tests/test-applications/node-firebase/createConfigFromEnv.js new file mode 100644 index 000000000000..3345a9d868fb --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-firebase/createConfigFromEnv.js @@ -0,0 +1,14 @@ +const path = require('path'); +const dotent = require('dotenv'); +dotent.config({ path: path.resolve(__dirname, './docker/.env') }); + +const createConfigFromEnv = require('./docker/firebase/utils').createConfigFromEnv; + +// eslint-disable-next-line @typescript-eslint/no-floating-promises +(async () => { + try { + await createConfigFromEnv(); + } catch (e) { + console.error(e); + } +})(); diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/createEnvFromConfig.js b/dev-packages/e2e-tests/test-applications/node-firebase/createEnvFromConfig.js new file mode 100644 index 000000000000..d19ea4cb1dc9 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-firebase/createEnvFromConfig.js @@ -0,0 +1,10 @@ +const createEnvFromConfig = require('./docker/firebase/utils').createEnvFromConfig; + +(async () => { + try { + await createEnvFromConfig(); + } catch (e) { + console.error(e); + } +})(); + diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/docker/.gitignore b/dev-packages/e2e-tests/test-applications/node-firebase/docker/.gitignore new file mode 100644 index 000000000000..b8b9f2089493 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-firebase/docker/.gitignore @@ -0,0 +1,6 @@ +.config +cache +firebase/data +firebase/firebase-export-* +*-debug.log +.env diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/docker/Dockerfile b/dev-packages/e2e-tests/test-applications/node-firebase/docker/Dockerfile new file mode 100644 index 000000000000..ce0fde56dbc7 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-firebase/docker/Dockerfile @@ -0,0 +1,15 @@ +FROM node:20-alpine + +ARG FIREBASE_VERSION + +RUN apk --no-cache add openjdk11-jre bash curl openssl gettext nano nginx sudo && \ + npm cache clean --force && \ + npm i -g firebase-tools@$FIREBASE_VERSION + +COPY nginx.conf /etc/nginx/ +COPY serve.sh /usr/bin/ +RUN chmod +x /usr/bin/serve.sh + +WORKDIR /srv/firebase + +ENTRYPOINT ["/usr/bin/serve.sh"] diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/docker/docker-compose.yml b/dev-packages/e2e-tests/test-applications/node-firebase/docker/docker-compose.yml new file mode 100644 index 000000000000..00b019398522 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-firebase/docker/docker-compose.yml @@ -0,0 +1,60 @@ +services: + emulator: + container_name: sentry-firebase + build: + context: . + dockerfile: Dockerfile + args: + - FIREBASE_VERSION=13.19.0 + stop_grace_period: 1m + environment: + FIREBASE_AUTH_EMULATOR_HOST: 'localhost:5507' + FIRESTORE_EMULATOR_HOST: 'localhost:5504' + PUBSUB_EMULATOR_HOST: 'localhost:5505' + FUNCTIONS_EMULATOR_HOST: 'localhost:5503' + FIREBASE_PROJECT: 'sentry-15d85' + GCLOUD_PROJECT: 'sentry-15d85' + FORCE_COLOR: 'true' + DATA_DIRECTORY: 'data' + CHOKIDAR_USEPOLLING: 'true' + CONFIG_FIREBASE_TOOLS: ${CONFIG_FIREBASE_TOOLS} + CONFIG_UPDATE_NOTIFIER_FIREBASE_TOOLS: ${CONFIG_UPDATE_NOTIFIER_FIREBASE_TOOLS} + ports: + - '5500:4001' # ui + - '5501:4401' # hub + - '5502:4601' # logging + - '5503:5002' # functions + - '5504:8081' # firestore + - '5505:8086' # pubsub + - '5506:9001' # database + - '5507:9100' # auth + - '5508:9200' # Storage + - '5509:6001' # Hosting + - '5510:9081' # firestore (grpc) + - '5511:9230' # cloud_functions_debug + - '9005:9005' # to be able to authenticate using gmail and docker + volumes: + - type: bind + source: ./firebase + target: /srv/firebase + bind: + create_host_path: true + - type: bind + source: ./cache + target: /root/.cache + bind: + create_host_path: true + - type: bind + source: .config + target: /root/.config + bind: + create_host_path: true + - type: bind + source: ./firebase/data + target: /srv/firebase/data + bind: + create_host_path: true + +networks: + default: + driver: bridge diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/.firebaserc b/dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/.firebaserc new file mode 100644 index 000000000000..392290d3c931 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/.firebaserc @@ -0,0 +1,5 @@ +{ + "projects": { + "default": "sentry-project-436908" + } +} diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/.gitignore b/dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/.gitignore new file mode 100644 index 000000000000..b17f63107554 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/.gitignore @@ -0,0 +1,69 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +firebase-debug.log* +firebase-debug.*.log* + +# Firebase cache +.firebase/ + +# Firebase config + +# Uncomment this if you'd like others to create their own Firebase project. +# For a team working on the same Firebase project(s), it is recommended to leave +# it commented so all members can deploy to the same project(s) in .firebaserc. +# .firebaserc + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +# dataconnect generated files +.dataconnect diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/createOrUpdateConfigIfPossible.js b/dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/createOrUpdateConfigIfPossible.js new file mode 100644 index 000000000000..3ac51191deab --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/createOrUpdateConfigIfPossible.js @@ -0,0 +1,3 @@ +const createOrUpdateConfigIfPossible = require('./utils').createOrUpdateConfigIfPossible; + +createOrUpdateConfigIfPossible(); diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/database.rules.json b/dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/database.rules.json new file mode 100644 index 000000000000..4335e236f6db --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/database.rules.json @@ -0,0 +1,7 @@ +{ + /* Visit https://firebase.google.com/docs/database/security to learn more about security rules. */ + "rules": { + ".read": false, + ".write": false + } +} diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/firebase.json b/dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/firebase.json new file mode 100644 index 000000000000..714e003f7324 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/firebase.json @@ -0,0 +1,25 @@ +{ + "database": { + "rules": "database.rules.json" + }, + "firestore": { + "rules": "firestore.rules", + "indexes": "firestore.indexes.json" + }, + "emulators": { + "auth": { + "port": 5507 + }, + "firestore": { + "port": 5504 + }, + "database": { + "port": 5506 + }, + "ui": { + "enabled": true, + "port": 5500 + }, + "singleProjectMode": true + } +} diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/firestore.indexes.json b/dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/firestore.indexes.json new file mode 100644 index 000000000000..415027e5ddaf --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/firestore.indexes.json @@ -0,0 +1,4 @@ +{ + "indexes": [], + "fieldOverrides": [] +} diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/firestore.rules b/dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/firestore.rules new file mode 100644 index 000000000000..c3c1733dcd8b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/firestore.rules @@ -0,0 +1,8 @@ +rules_version = '2'; +service cloud.firestore { + match /databases/{database}/documents { + match /{document=**} { + allow read, write: if true; + } + } +} diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/storage.rules b/dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/storage.rules new file mode 100644 index 000000000000..4cc806e7d390 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/storage.rules @@ -0,0 +1,8 @@ +rules_version = '2'; +service firebase.storage { + match /b/{bucket}/o { + match /{file=**} { + allow read, write: if true; + } + } +} diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/utils.js b/dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/utils.js new file mode 100644 index 000000000000..0fc6069c682e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/utils.js @@ -0,0 +1,145 @@ +const fs = require('fs'); +const path = require('path'); + +const firebaseToolsFileName = 'firebase-tools.json'; +const updateNotifierFirebaseToolsFileName = 'update-notifier-firebase-tools.json'; + +function createJsonFile(filePath, json) { + return new Promise((resolve, reject) => { + let content = JSON.stringify(json, null, 2); + + // replace spaces with tabs + content = content.replace(/[ ]{2}/g, '\t'); + + fs.mkdirSync(filePath.substring(0, filePath.lastIndexOf('/')), { recursive: true }); + fs.writeFile(filePath, content, function (err) { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); +} + +function createEnvironmentFile(filePath, content) { + return new Promise((resolve, reject) => { + fs.writeFile(filePath, content, function (err) { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); +} + +function readJsonFromFile(filePath) { + return new Promise((resolve, reject) => { + fs.readFile(filePath, 'utf8', (err, data) => { + if (err) { + reject(err); + return; + } + try { + resolve(JSON.parse(data)); + } catch (err) { + reject(err); + console.error(err); + } + }); + }); +} + +/** + * Creates firebase configuration based on .env file + */ +async function createConfigFromEnv() { + const dockerPath = path.resolve(__dirname, '..'); + const configPath = path.resolve(dockerPath, '.config/configstore'); + + let filePathFirebaseTools = process.env.CONFIG_FIREBASE_TOOLS; + let filePathUpdateNotifierFirebaseTools = process.env.CONFIG_UPDATE_NOTIFIER_FIREBASE_TOOLS; + + if (typeof filePathFirebaseTools !== 'string') { + throw new Error('no CONFIG_FIREBASE_TOOLS environment'); + } + if (typeof filePathUpdateNotifierFirebaseTools !== 'string') { + throw new Error('no CONFIG_UPDATE_NOTIFIER_FIREBASE_TOOLS environment'); + } + + try { + filePathFirebaseTools = JSON.parse(filePathFirebaseTools); + filePathUpdateNotifierFirebaseTools = JSON.parse(filePathUpdateNotifierFirebaseTools); + + await Promise.all([ + createJsonFile(path.resolve(configPath, firebaseToolsFileName), filePathFirebaseTools), + createJsonFile( + path.resolve(configPath, updateNotifierFirebaseToolsFileName), + filePathUpdateNotifierFirebaseTools, + ), + ]); + console.log('firebase config based on environment variables created successfully'); + } catch (e) { + console.error('firebase config creation error', e); + } +} + +/** + * Creates file .env based on firebase configuration + */ +async function createEnvFromConfig() { + const dockerPath = path.resolve(__dirname, '..'); + const configPath = path.resolve(dockerPath, '.config/configstore'); + const dockerFilePath = path.resolve(dockerPath, '.env'); + try { + const results = await Promise.all([ + readJsonFromFile(path.resolve(configPath, firebaseToolsFileName)), + readJsonFromFile(path.resolve(configPath, updateNotifierFirebaseToolsFileName)), + ]); + const filePathFirebaseTools = results[0]; + const filePathUpdateNotifierFirebaseTools = results[1]; + + if (!filePathFirebaseTools) { + throw new Error('Environment variable '); + } + + const content = []; + content.push('# This is autogenerated'); + content.push(`CONFIG_FIREBASE_TOOLS=${JSON.stringify(filePathFirebaseTools)}`); + content.push(`CONFIG_UPDATE_NOTIFIER_FIREBASE_TOOLS=${JSON.stringify(filePathUpdateNotifierFirebaseTools)}`); + content.push(''); + await createEnvironmentFile(dockerFilePath, content.join('\n')); + + console.log(`environments variables saved in "${dockerFilePath}" based on firebase config`); + } catch (e) { + console.error(e); + } +} + +/** + * Creates or update the existing config whenever environment is defined. This is used by docker and will recreate + * the config each time the docker is run as long as the environment settings exist. + */ +function createOrUpdateConfigIfPossible() { + const filePathFirebaseTools = process.env.CONFIG_FIREBASE_TOOLS; + const filePathUpdateNotifierFirebaseTools = process.env.CONFIG_UPDATE_NOTIFIER_FIREBASE_TOOLS; + if ( + typeof filePathFirebaseTools === 'string' && + typeof filePathUpdateNotifierFirebaseTools === 'string' && + filePathFirebaseTools !== '' && + filePathUpdateNotifierFirebaseTools !== '' + ) { + createConfigFromEnv(); + } else { + console.error('>>>>>>>>>>>>>>>> WARNING <<<<<<<<<<<<<<<<<<<<<'); + console.error('firebase config creation failed due to missing environment variables'); + console.error('>>>>>>>>>>>>>>>> WARNING <<<<<<<<<<<<<<<<<<<<<'); + } +} + +module.exports = { + createEnvFromConfig: createEnvFromConfig, + createConfigFromEnv: createConfigFromEnv, + createOrUpdateConfigIfPossible: createOrUpdateConfigIfPossible, +}; diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/docker/nginx.conf b/dev-packages/e2e-tests/test-applications/node-firebase/docker/nginx.conf new file mode 100644 index 000000000000..d8189878c99a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-firebase/docker/nginx.conf @@ -0,0 +1,150 @@ +daemon off; +worker_processes 1; + +# error_log /dev/stderr; + +events { + worker_connections 1024; +} + +http { + client_max_body_size 100M; + + map $http_upgrade $connection_upgrade { + default upgrade; '' close; + } + + server { + listen 0.0.0.0:4001; + server_name ui; + location / { + proxy_pass http://127.0.0.1:5500; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Host $host; + } + } + + server { + listen 0.0.0.0:4401; + server_name hub; + location / { + proxy_pass http://127.0.0.1:5501; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Host $host; + } + } + + server { + listen 0.0.0.0:4601; + server_name logging; + location / { + proxy_pass http://127.0.0.1:5502; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Host $host; + } + } + + server { + listen 0.0.0.0:5002; + server_name functions; + location / { + proxy_pass http://127.0.0.1:5503; + } + } + + server { + listen 0.0.0.0:8081; + server_name firestore; + location / { + proxy_pass http://127.0.0.1:5504; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Host $host; + } + } + + server { + listen 0.0.0.0:8086; + http2 on; + server_name pubsub; + location / { + grpc_pass grpc://127.0.0.1:5505; + } + } + + server { + listen 0.0.0.0:9001; + server_name database; + location / { + proxy_pass http://127.0.0.1:5506; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Host $host; + } + } + + server { + listen 0.0.0.0:9100; + server_name auth; + location / { + proxy_pass http://127.0.0.1:5507; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Host $host; + } + } + + server { + listen 0.0.0.0:9200; + server_name cloud_storage; + location / { + proxy_pass http://127.0.0.1:5508; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Host $host; + } + } + + server { + listen 0.0.0.0:6001; + server_name hosting; + location / { + proxy_pass http://127.0.0.1:5509; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Host $host; + } + } + + server { + listen 0.0.0.0:9081; + http2 on; + server_name firestore; + location / { + grpc_pass grpc://127.0.0.1:5510; + } + } + + server { + listen 0.0.0.0:9230; + server_name cloud_functions_debug; + location / { + proxy_pass http://127.0.0.1:5511; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Host $host; + } + } +} diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/docker/serve.sh b/dev-packages/e2e-tests/test-applications/node-firebase/docker/serve.sh new file mode 100644 index 000000000000..23503e69109c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-firebase/docker/serve.sh @@ -0,0 +1,64 @@ +#!/bin/bash +set -eo pipefail + +# Function to print error messages and exit +error_exit() { + echo "$1" 1>&2 + exit 1 +} + +# Sanity checks +[[ -z "${DATA_DIRECTORY}" ]] && echo "DATA_DIRECTORY environment variable missing, will not export or import data to firebase" +[[ -z "${FIREBASE_PROJECT}" ]] && error_exit "FIREBASE_PROJECT environment variable missing" + +dirs=("/srv/firebase/functions" "/srv/firebase/firestore" "/srv/firebase/storage") + +for dir in "${dirs[@]}"; do + if [[ -d "$dir" ]]; then + echo "Installing npm packages in $dir" + npm install --prefix "$dir" || error_exit "npm install failed in $dir" + fi +done + +if [[ -z "${CONFIG_FIREBASE_TOOLS}" ]];then + echo "CONFIG_FIREBASE_TOOLS environment variable missing" +fi +if [[ -z "${CONFIG_UPDATE_NOTIFIER_FIREBASE_TOOLS}" ]];then + echo "CONFIG_UPDATE_NOTIFIER_FIREBASE_TOOLS environment variable missing" +fi + +node createOrUpdateConfigIfPossible.js +sleep 1 + +# Start Firebase emulators +emulator_cmd="firebase emulators:start --project=${FIREBASE_PROJECT}" +[[ -n "${DATA_DIRECTORY}" ]] && emulator_cmd+=" --import=./${DATA_DIRECTORY}/export --export-on-exit" +$emulator_cmd & +firebase_pid=$! + +# Start nginx and npm +echo "Starting nginx..." +nginx & +nginx_pid=$! + +cleanup() { + echo "Stopping services..." + # Gracefully stop background processes + echo "Terminating background services..." + if [[ -n "$firebase_pid" ]]; then + kill -SIGTERM "$firebase_pid" || echo "Failed to terminate Firebase process" + wait "$firebase_pid" 2>/dev/null + fi + if [[ -n "$nginx_pid" ]]; then + kill -SIGTERM "$nginx_pid" || echo "Failed to terminate Nginx process" + wait "$nginx_pid" 2>/dev/null + fi + if [[ -n "$npm_pid" ]]; then + kill -SIGTERM "$npm_pid" || echo "Failed to terminate NPM process" + wait "$npm_pid" 2>/dev/null + fi +} + +trap cleanup INT TERM SIGTERM SIGINT + +wait diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/package.json b/dev-packages/e2e-tests/test-applications/node-firebase/package.json new file mode 100644 index 000000000000..ad47dfd77c7e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-firebase/package.json @@ -0,0 +1,34 @@ +{ + "name": "node-firebase-e2e-test-app", + "version": "0.0.1", + "private": true, + "scripts": { + "build": "tsc", + "createEnvFromConfig": "node createEnvFromConfig.js", + "createConfigFromEnv": "node createConfigFromEnv.js", + "dev": "tsc --build --watch", + "docker": "cd docker && docker compose up --build -d", + "proxy": "node start-event-proxy.mjs", + "start": "node ./dist/app.js", + "test": "playwright test", + "clean": "npx rimraf node_modules pnpm-lock.yaml", + "test:build": "pnpm install && pnpm docker && pnpm build", + "test:assert": "pnpm test" + }, + "dependencies": { + "@sentry/node": "latest || *", + "@sentry/core": "latest || *", + "@sentry/opentelemetry": "latest || *", + "dotenv": "^16.4.5", + "firebase": "^11.0.1", + "tsconfig-paths": "^4.2.0", + "typescript": "4.9.5" + }, + "devDependencies": { + "@playwright/test": "~1.50.0", + "@sentry-internal/test-utils": "link:../../../test-utils" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/node-firebase/playwright.config.mjs new file mode 100644 index 000000000000..31f2b913b58b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-firebase/playwright.config.mjs @@ -0,0 +1,7 @@ +import { getPlaywrightConfig } from '@sentry-internal/test-utils'; + +const config = getPlaywrightConfig({ + startCommand: `pnpm start`, +}); + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/src/app.ts b/dev-packages/e2e-tests/test-applications/node-firebase/src/app.ts new file mode 100644 index 000000000000..be7a18b7bca6 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-firebase/src/app.ts @@ -0,0 +1,71 @@ +import * as Sentry from '@sentry/node'; +import './init'; +import express from 'express'; +import type { FirebaseOptions } from '@firebase/app'; +import { initializeApp } from 'firebase/app'; +import { + addDoc, + collection, + connectFirestoreEmulator, + deleteDoc, + doc, + getDocs, + getFirestore, + setDoc, +} from 'firebase/firestore/lite'; // seems like "firebase/firestore" is trying to use grpc for connection and it + +const options: FirebaseOptions = { + projectId: 'sentry-15d85', + apiKey: 'sentry-fake-api-key', +}; + +const app = initializeApp(options); + +const db = getFirestore(app); +connectFirestoreEmulator(db, '127.0.0.1', 5504); +const citiesRef = collection(db, 'cities'); + +async function addCity(): Promise { + await addDoc(citiesRef, { + name: 'San Francisco', + }); +} + +async function getCities(): Promise { + const citySnapshot = await getDocs(citiesRef); + const cityList = citySnapshot.docs.map(doc => doc.data()); + return cityList; +} + +async function deleteCity(): Promise { + await deleteDoc(doc(citiesRef, 'SF')); +} + +async function setCity(): Promise { + await setDoc(doc(citiesRef, 'SF'), { + name: 'San Francisco', + state: 'CA', + country: 'USA', + capital: false, + population: 860000, + regions: ['west_coast', 'norcal'], + }); +} + +const expressApp = express(); +const port = 3030; + +expressApp.get('/test', async function (req, res) { + await Sentry.startSpan({ name: 'Test Transaction' }, async () => { + await addCity(); + await setCity(); + await getCities(); + await deleteCity(); + }); + await Sentry.flush(); + res.send({ version: 'v1' }); +}); + +expressApp.listen(port, () => { + console.log(`Example app listening on port ${port}`); +}); diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/src/init.ts b/dev-packages/e2e-tests/test-applications/node-firebase/src/init.ts new file mode 100644 index 000000000000..23c3d2fa5974 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-firebase/src/init.ts @@ -0,0 +1,11 @@ +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/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/node-firebase/start-event-proxy.mjs new file mode 100644 index 000000000000..d935bf3dcc0b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-firebase/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'node-firebase', +}); diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-firebase/tests/transactions.test.ts new file mode 100644 index 000000000000..879606105a13 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-firebase/tests/transactions.test.ts @@ -0,0 +1,117 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +const spanAddDoc = expect.objectContaining({ + description: 'addDoc cities', + data: expect.objectContaining({ + 'db.collection.name': 'cities', + 'db.namespace': '[DEFAULT]', + 'db.operation.name': 'addDoc', + 'db.system.name': 'firebase.firestore', + 'firebase.firestore.options.projectId': 'sentry-15d85', + 'firebase.firestore.type': 'collection', + 'otel.kind': 'CLIENT', + 'server.address': '127.0.0.1', + 'server.port': '5504', + 'sentry.origin': 'auto.firebase.otel.firestore', + 'sentry.op': 'db.query', + }), + op: 'db.query', + origin: 'auto.firebase.otel.firestore', + parent_span_id: expect.any(String), + trace_id: expect.any(String), + span_id: expect.any(String), + timestamp: expect.any(Number), + start_timestamp: expect.any(Number), + status: 'ok', +}); + +const spanSetDocs = expect.objectContaining({ + description: 'setDocs cities', + data: expect.objectContaining({ + 'db.collection.name': 'cities', + 'db.namespace': '[DEFAULT]', + 'db.operation.name': 'setDocs', + 'db.system.name': 'firebase.firestore', + 'firebase.firestore.options.projectId': 'sentry-15d85', + 'firebase.firestore.type': 'collection', + 'otel.kind': 'CLIENT', + 'server.address': '127.0.0.1', + 'server.port': '5504', + 'sentry.origin': 'auto.firebase.otel.firestore', + 'sentry.op': 'db.query', + }), + op: 'db.query', + origin: 'auto.firebase.otel.firestore', + parent_span_id: expect.any(String), + trace_id: expect.any(String), + span_id: expect.any(String), + timestamp: expect.any(Number), + start_timestamp: expect.any(Number), + status: 'ok', +}); + +const spanGetDocs = expect.objectContaining({ + description: 'getDocs cities', + data: expect.objectContaining({ + 'db.collection.name': 'cities', + 'db.namespace': '[DEFAULT]', + 'db.operation.name': 'getDocs', + 'db.system.name': 'firebase.firestore', + 'firebase.firestore.options.projectId': 'sentry-15d85', + 'firebase.firestore.type': 'collection', + 'otel.kind': 'CLIENT', + 'server.address': '127.0.0.1', + 'server.port': '5504', + 'sentry.origin': 'auto.firebase.otel.firestore', + 'sentry.op': 'db.query', + }), + op: 'db.query', + origin: 'auto.firebase.otel.firestore', + parent_span_id: expect.any(String), + trace_id: expect.any(String), + span_id: expect.any(String), + timestamp: expect.any(Number), + start_timestamp: expect.any(Number), + status: 'ok', +}); + +const spanDeleteDoc = expect.objectContaining({ + description: 'deleteDoc cities', + data: expect.objectContaining({ + 'db.collection.name': 'cities', + 'db.namespace': '[DEFAULT]', + 'db.operation.name': 'deleteDoc', + 'db.system.name': 'firebase.firestore', + 'firebase.firestore.options.projectId': 'sentry-15d85', + 'firebase.firestore.type': 'collection', + 'otel.kind': 'CLIENT', + 'server.address': '127.0.0.1', + 'server.port': '5504', + 'sentry.origin': 'auto.firebase.otel.firestore', + 'sentry.op': 'db.query', + }), + op: 'db.query', + origin: 'auto.firebase.otel.firestore', + parent_span_id: expect.any(String), + trace_id: expect.any(String), + span_id: expect.any(String), + timestamp: expect.any(Number), + start_timestamp: expect.any(Number), + status: 'ok', +}); + +test('should add, set, get and delete document', async ({ baseURL, page }) => { + const serverTransactionPromise = waitForTransaction('node-firebase', span => { + return span.transaction === 'Test Transaction'; + }); + + await fetch(`${baseURL}/test`); + + const transactionEvent = await serverTransactionPromise; + + expect(transactionEvent.transaction).toEqual('Test Transaction'); + expect(transactionEvent.spans?.length).toEqual(4); + + expect(transactionEvent.spans).toEqual(expect.arrayContaining([spanAddDoc, spanSetDocs, spanGetDocs, spanDeleteDoc])); +}); 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 new file mode 100644 index 000000000000..26c30d4eddf2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-firebase/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "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 new file mode 100644 index 000000000000..8cb64e989ed9 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-firebase/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "types": ["node"], + "esModuleInterop": true, + "lib": ["es2018"], + "strict": true, + "outDir": "dist" + }, + "include": ["src/**/*.ts"] +} diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 4e7a8482c474..bba0f98bc75e 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -32,6 +32,7 @@ export { statsigIntegration, unleashIntegration, } from './integrations/featureFlagShims'; +export { firebaseIntegration } from './integrations/tracing/firebase'; export { init, diff --git a/packages/node/src/integrations/tracing/firebase/README.md b/packages/node/src/integrations/tracing/firebase/README.md new file mode 100644 index 000000000000..6d839a4476f5 --- /dev/null +++ b/packages/node/src/integrations/tracing/firebase/README.md @@ -0,0 +1 @@ +The structure inside OTEL is to be kept as close as possible to opentelemetry plugin. diff --git a/packages/node/src/integrations/tracing/firebase/firebase.ts b/packages/node/src/integrations/tracing/firebase/firebase.ts new file mode 100644 index 000000000000..c2e701d24e9a --- /dev/null +++ b/packages/node/src/integrations/tracing/firebase/firebase.ts @@ -0,0 +1,29 @@ +import type { Span } from '@opentelemetry/api'; +import type { IntegrationFn } from '@sentry/core'; +import { defineIntegration, SEMANTIC_ATTRIBUTE_SENTRY_OP } from '@sentry/core'; +import { generateInstrumentOnce } from '../../../otel/instrument'; +import { addOriginToSpan } from '../../../utils/addOriginToSpan'; +import { type FirebaseInstrumentationConfig, FirebaseInstrumentation } from './otel'; + +const INTEGRATION_NAME = 'Firebase'; + +const config: FirebaseInstrumentationConfig = { + firestoreSpanCreationHook: span => { + addOriginToSpan(span as Span, 'auto.firebase.otel.firestore'); + + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'db.query'); + }, +}; + +export const instrumentFirebase = generateInstrumentOnce(INTEGRATION_NAME, () => new FirebaseInstrumentation(config)); + +const _firebaseIntegration = (() => { + return { + name: INTEGRATION_NAME, + setupOnce() { + instrumentFirebase(); + }, + }; +}) satisfies IntegrationFn; + +export const firebaseIntegration = defineIntegration(_firebaseIntegration); diff --git a/packages/node/src/integrations/tracing/firebase/index.ts b/packages/node/src/integrations/tracing/firebase/index.ts new file mode 100644 index 000000000000..5588511bf303 --- /dev/null +++ b/packages/node/src/integrations/tracing/firebase/index.ts @@ -0,0 +1 @@ +export * from './firebase'; diff --git a/packages/node/src/integrations/tracing/firebase/otel/firebaseInstrumentation.ts b/packages/node/src/integrations/tracing/firebase/otel/firebaseInstrumentation.ts new file mode 100644 index 000000000000..5447eeda552f --- /dev/null +++ b/packages/node/src/integrations/tracing/firebase/otel/firebaseInstrumentation.ts @@ -0,0 +1,37 @@ +import { type InstrumentationNodeModuleDefinition, InstrumentationBase } from '@opentelemetry/instrumentation'; +import { SDK_VERSION } from '@sentry/core'; +import { patchFirestore } from './patches/firestore'; +import type { FirebaseInstrumentationConfig } from './types'; + +const DefaultFirebaseInstrumentationConfig: FirebaseInstrumentationConfig = {}; +const firestoreSupportedVersions = ['>=3.0.0 <5']; // firebase 9+ + +/** + * + */ +export class FirebaseInstrumentation extends InstrumentationBase { + public constructor(config: FirebaseInstrumentationConfig = DefaultFirebaseInstrumentationConfig) { + super('@sentry/instrumentation-firebase', SDK_VERSION, config); + } + + /** + * sets config + * @param config + */ + public override setConfig(config: FirebaseInstrumentationConfig = {}): void { + super.setConfig({ ...DefaultFirebaseInstrumentationConfig, ...config }); + } + + /** + * + * @protected + */ + // eslint-disable-next-line @typescript-eslint/naming-convention + protected init(): InstrumentationNodeModuleDefinition | InstrumentationNodeModuleDefinition[] | void { + const modules: InstrumentationNodeModuleDefinition[] = []; + + modules.push(patchFirestore(this.tracer, firestoreSupportedVersions, this._wrap, this._unwrap, this.getConfig())); + + return modules; + } +} diff --git a/packages/node/src/integrations/tracing/firebase/otel/index.ts b/packages/node/src/integrations/tracing/firebase/otel/index.ts new file mode 100644 index 000000000000..3b914e641ec0 --- /dev/null +++ b/packages/node/src/integrations/tracing/firebase/otel/index.ts @@ -0,0 +1,2 @@ +export * from './firebaseInstrumentation'; +export * from './types'; diff --git a/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts b/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts new file mode 100644 index 000000000000..e4fa56cca4a5 --- /dev/null +++ b/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts @@ -0,0 +1,291 @@ +import type { Span, Tracer } from '@opentelemetry/api'; +import { context, diag, SpanKind, trace } from '@opentelemetry/api'; +import { + InstrumentationNodeModuleDefinition, + InstrumentationNodeModuleFile, + isWrapped, + safeExecuteInTheMiddle, +} from '@opentelemetry/instrumentation'; +import { + ATTR_DB_COLLECTION_NAME, + ATTR_DB_NAMESPACE, + ATTR_DB_OPERATION_NAME, + ATTR_DB_SYSTEM_NAME, + ATTR_SERVER_ADDRESS, + ATTR_SERVER_PORT, +} from '@opentelemetry/semantic-conventions'; +import type { SpanAttributes } from '@sentry/core'; +import type { unwrap as shimmerUnwrap, wrap as shimmerWrap } from 'shimmer'; +import type { FirebaseInstrumentation } from '../firebaseInstrumentation'; +import type { + AddDocType, + CollectionReference, + DeleteDocType, + DocumentData, + DocumentReference, + FirebaseApp, + FirebaseInstrumentationConfig, + FirebaseOptions, + FirestoreSettings, + FirestoreSpanCreationHook, + GetDocsType, + PartialWithFieldValue, + QuerySnapshot, + SetDocType, + SetOptions, + WithFieldValue, +} from '../types'; + +/** + * + * @param tracer - Opentelemetry Tracer + * @param firestoreSupportedVersions - supported version of firebase/firestore + * @param wrap - reference to native instrumentation wrap function + * @param unwrap - reference to native instrumentation wrap function + */ +export function patchFirestore( + tracer: Tracer, + firestoreSupportedVersions: string[], + wrap: typeof shimmerWrap, + unwrap: typeof shimmerUnwrap, + config: FirebaseInstrumentationConfig, +): InstrumentationNodeModuleDefinition { + // eslint-disable-next-line @typescript-eslint/no-empty-function + const defaultFirestoreSpanCreationHook: FirestoreSpanCreationHook = () => {}; + + let firestoreSpanCreationHook: FirestoreSpanCreationHook = defaultFirestoreSpanCreationHook; + const configFirestoreSpanCreationHook = config.firestoreSpanCreationHook; + + if (typeof configFirestoreSpanCreationHook === 'function') { + firestoreSpanCreationHook = (span: Span) => { + safeExecuteInTheMiddle( + () => configFirestoreSpanCreationHook(span), + error => { + if (!error) { + return; + } + diag.error(error?.message); + }, + true, + ); + }; + } + + const moduleFirestoreCJS = new InstrumentationNodeModuleDefinition( + '@firebase/firestore', + firestoreSupportedVersions, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (moduleExports: any) => wrapMethods(moduleExports, wrap, unwrap, tracer, firestoreSpanCreationHook), + ); + const files: string[] = [ + '@firebase/firestore/dist/lite/index.node.cjs.js', + '@firebase/firestore/dist/lite/index.node.mjs.js', + '@firebase/firestore/dist/lite/index.rn.esm2017.js', + '@firebase/firestore/dist/lite/index.cjs.js', + ]; + + for (const file of files) { + moduleFirestoreCJS.files.push( + new InstrumentationNodeModuleFile( + file, + firestoreSupportedVersions, + moduleExports => wrapMethods(moduleExports, wrap, unwrap, tracer, firestoreSpanCreationHook), + moduleExports => unwrapMethods(moduleExports, unwrap), + ), + ); + } + + return moduleFirestoreCJS; +} + +function wrapMethods( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + moduleExports: any, + wrap: typeof shimmerWrap, + unwrap: typeof shimmerUnwrap, + tracer: Tracer, + firestoreSpanCreationHook: FirestoreSpanCreationHook, + // eslint-disable-next-line @typescript-eslint/no-explicit-any +): any { + unwrapMethods(moduleExports, unwrap); + + wrap(moduleExports, 'addDoc', patchAddDoc(tracer, firestoreSpanCreationHook)); + wrap(moduleExports, 'getDocs', patchGetDocs(tracer, firestoreSpanCreationHook)); + wrap(moduleExports, 'setDoc', patchSetDoc(tracer, firestoreSpanCreationHook)); + wrap(moduleExports, 'deleteDoc', patchDeleteDoc(tracer, firestoreSpanCreationHook)); + + return moduleExports; +} + +function unwrapMethods( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + moduleExports: any, + unwrap: typeof shimmerUnwrap, + // eslint-disable-next-line @typescript-eslint/no-explicit-any +): any { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + if (isWrapped(moduleExports.addDoc)) { + unwrap(moduleExports, 'addDoc'); + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + if (isWrapped(moduleExports.getDocs)) { + unwrap(moduleExports, 'getDocs'); + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + if (isWrapped(moduleExports.setDoc)) { + unwrap(moduleExports, 'setDoc'); + } + + return moduleExports; +} + +function patchAddDoc( + tracer: Tracer, + firestoreSpanCreationHook: FirestoreSpanCreationHook, +): ( + original: AddDocType, +) => ( + this: FirebaseInstrumentation, + reference: CollectionReference, + data: WithFieldValue, +) => Promise> { + return function addDoc(original: AddDocType) { + return function patchAddDoc( + reference: CollectionReference, + data: WithFieldValue, + ): Promise> { + const span = startSpan(tracer, 'addDoc', reference); + firestoreSpanCreationHook(span); + return executeContextWithSpan>>(span, () => { + return original(reference, data); + }); + }; + }; +} + +function patchDeleteDoc( + tracer: Tracer, + firestoreSpanCreationHook: FirestoreSpanCreationHook, +): ( + original: DeleteDocType, +) => (this: FirebaseInstrumentation, reference: DocumentReference) => Promise { + return function deleteDoc(original: DeleteDocType) { + return function patchDeleteDoc(reference: DocumentReference): Promise { + const span = startSpan(tracer, 'deleteDoc', reference.parent || reference); + firestoreSpanCreationHook(span); + return executeContextWithSpan>(span, () => { + return original(reference); + }); + }; + }; +} + +function patchGetDocs( + tracer: Tracer, + firestoreSpanCreationHook: FirestoreSpanCreationHook, +): ( + original: GetDocsType, +) => ( + this: FirebaseInstrumentation, + reference: CollectionReference, +) => Promise> { + return function getDocs(original: GetDocsType) { + return function patchGetDocs( + reference: CollectionReference, + ): Promise> { + const span = startSpan(tracer, 'getDocs', reference); + firestoreSpanCreationHook(span); + return executeContextWithSpan>>(span, () => { + return original(reference); + }); + }; + }; +} + +function patchSetDoc( + tracer: Tracer, + firestoreSpanCreationHook: FirestoreSpanCreationHook, +): ( + original: SetDocType, +) => ( + this: FirebaseInstrumentation, + reference: DocumentReference, + data: WithFieldValue & PartialWithFieldValue, + options?: SetOptions, +) => Promise { + return function setDoc(original: SetDocType) { + return function patchSetDoc( + reference: DocumentReference, + data: WithFieldValue & PartialWithFieldValue, + options?: SetOptions, + ): Promise { + const span = startSpan(tracer, 'setDocs', reference.parent || reference); + firestoreSpanCreationHook(span); + + return executeContextWithSpan>(span, () => { + return typeof options !== 'undefined' ? original(reference, data, options) : original(reference, data); + }); + }; + }; +} + +function executeContextWithSpan(span: Span, callback: () => T): T { + return context.with(trace.setSpan(context.active(), span), () => { + return safeExecuteInTheMiddle( + (): T => { + return callback(); + }, + err => { + if (err) { + span.recordException(err); + } + span.end(); + }, + true, + ); + }); +} + +function startSpan( + tracer: Tracer, + spanName: string, + reference: CollectionReference | DocumentReference, +): Span { + const span = tracer.startSpan(`${spanName} ${reference.path}`, { kind: SpanKind.CLIENT }); + addAttributes(span, reference); + span.setAttribute(ATTR_DB_OPERATION_NAME, spanName); + return span; +} + +function addAttributes( + span: Span, + reference: CollectionReference | DocumentReference, +): void { + const firestoreApp: FirebaseApp = reference.firestore.app; + const firestoreOptions: FirebaseOptions = firestoreApp.options; + const json: { settings?: FirestoreSettings } = reference.firestore.toJSON() || {}; + const settings: FirestoreSettings = json.settings || {}; + + const attributes: SpanAttributes = { + [ATTR_DB_COLLECTION_NAME]: reference.path, + [ATTR_DB_NAMESPACE]: firestoreApp.name, + [ATTR_DB_SYSTEM_NAME]: 'firebase.firestore', + 'firebase.firestore.type': reference.type, + 'firebase.firestore.options.projectId': firestoreOptions.projectId, + 'firebase.firestore.options.appId': firestoreOptions.appId, + 'firebase.firestore.options.messagingSenderId': firestoreOptions.messagingSenderId, + 'firebase.firestore.options.storageBucket': firestoreOptions.storageBucket, + }; + + if (typeof settings.host === 'string') { + const arr = settings.host.split(':'); + if (arr.length === 2) { + attributes[ATTR_SERVER_ADDRESS] = arr[0]; + attributes[ATTR_SERVER_PORT] = arr[1]; + } + } + + span.setAttributes(attributes); +} diff --git a/packages/node/src/integrations/tracing/firebase/otel/types.ts b/packages/node/src/integrations/tracing/firebase/otel/types.ts new file mode 100644 index 000000000000..d6efadaa77e7 --- /dev/null +++ b/packages/node/src/integrations/tracing/firebase/otel/types.ts @@ -0,0 +1,119 @@ +import type { Span } from '@opentelemetry/api'; +import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; + +// Inlined types from 'firebase/app' +export interface FirebaseOptions { + [key: string]: any; + apiKey?: string; + authDomain?: string; + databaseURL?: string; + projectId?: string; + storageBucket?: string; + messagingSenderId?: string; + appId?: string; + measurementId?: string; +} + +export interface FirebaseApp { + name: string; + options: FirebaseOptions; + automaticDataCollectionEnabled: boolean; + delete(): Promise; +} + +// Inlined types from 'firebase/firestore' +export interface DocumentData { + [field: string]: any; +} + +export type WithFieldValue = T; + +export type PartialWithFieldValue = Partial; + +export interface SetOptions { + merge?: boolean; + mergeFields?: (string | number | symbol)[]; +} + +export interface DocumentReference { + id: string; + firestore: { + app: FirebaseApp; + settings: FirestoreSettings; + useEmulator: (host: string, port: number) => void; + toJSON: () => { + app: FirebaseApp; + settings: FirestoreSettings; + }; + }; + type: string; // 'collection' or 'document' + path: string; + parent: CollectionReference; +} + +export interface CollectionReference { + id: string; + firestore: { + app: FirebaseApp; + settings: FirestoreSettings; + useEmulator: (host: string, port: number) => void; + toJSON: () => { + app: FirebaseApp; + settings: FirestoreSettings; + }; + }; + type: string; // 'collection' or 'document' + path: string; + parent: DocumentReference | null; +} + +export interface QuerySnapshot { + docs: Array>; + size: number; + empty: boolean; +} + +export interface FirestoreSettings { + host?: string; + ssl?: boolean; + ignoreUndefinedProperties?: boolean; + cacheSizeBytes?: number; + experimentalForceLongPolling?: boolean; + experimentalAutoDetectLongPolling?: boolean; + useFetchStreams?: boolean; +} + +/** + * Firebase Auto Instrumentation + */ +export interface FirebaseInstrumentationConfig extends InstrumentationConfig { + firestoreSpanCreationHook?: FirestoreSpanCreationHook; +} + +export interface FirestoreSpanCreationHook { + (span: Span): void; +} + +// Function types (addDoc, getDocs, setDoc, deleteDoc) are defined below as types +export type GetDocsType = ( + query: CollectionReference, +) => Promise>; + +export type SetDocType = (( + reference: DocumentReference, + data: WithFieldValue, +) => Promise) & + (( + reference: DocumentReference, + data: PartialWithFieldValue, + options: SetOptions, + ) => Promise); + +export type AddDocType = ( + reference: CollectionReference, + data: WithFieldValue, +) => Promise>; + +export type DeleteDocType = ( + reference: DocumentReference, +) => Promise; diff --git a/packages/node/src/integrations/tracing/index.ts b/packages/node/src/integrations/tracing/index.ts index f27b3cf615e8..6035cf3669f8 100644 --- a/packages/node/src/integrations/tracing/index.ts +++ b/packages/node/src/integrations/tracing/index.ts @@ -4,6 +4,7 @@ import { amqplibIntegration, instrumentAmqplib } from './amqplib'; import { connectIntegration, instrumentConnect } from './connect'; import { expressIntegration, instrumentExpress } from './express'; import { fastifyIntegration, instrumentFastify, instrumentFastifyV3 } from './fastify'; +import { firebaseIntegration, instrumentFirebase } from './firebase'; import { genericPoolIntegration, instrumentGenericPool } from './genericPool'; import { graphqlIntegration, instrumentGraphql } from './graphql'; import { hapiIntegration, instrumentHapi } from './hapi'; @@ -48,6 +49,7 @@ export function getAutoPerformanceIntegrations(): Integration[] { vercelAIIntegration(), openAIIntegration(), postgresJsIntegration(), + firebaseIntegration(), ]; } @@ -80,5 +82,6 @@ export function getOpenTelemetryInstrumentationToPreload(): (((options?: any) => instrumentVercelAi, instrumentOpenAi, instrumentPostgresJs, + instrumentFirebase, ]; } From 07c9e9b50f0674e48fc8583e96e127c577ac3e2c Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Tue, 24 Jun 2025 15:31:23 +0100 Subject: [PATCH 02/20] Add missing dependency --- .../e2e-tests/test-applications/node-firebase/package.json | 1 + 1 file changed, 1 insertion(+) 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 ad47dfd77c7e..6d1e93d917f8 100644 --- a/dev-packages/e2e-tests/test-applications/node-firebase/package.json +++ b/dev-packages/e2e-tests/test-applications/node-firebase/package.json @@ -16,6 +16,7 @@ "test:assert": "pnpm test" }, "dependencies": { + "@firebase/app": "^0.13.1", "@sentry/node": "latest || *", "@sentry/core": "latest || *", "@sentry/opentelemetry": "latest || *", From 5a9cc23269abcbb7cf843a14b872dd4469e27bcf Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Tue, 24 Jun 2025 15:39:35 +0100 Subject: [PATCH 03/20] Add missing node re-exports --- packages/astro/src/index.server.ts | 1 + packages/aws-serverless/src/index.ts | 1 + packages/bun/src/index.ts | 1 + packages/google-cloud-serverless/src/index.ts | 1 + 4 files changed, 4 insertions(+) diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index 0b92c8a4a6f8..a9e81aee7db5 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -39,6 +39,7 @@ export { expressIntegration, extraErrorDataIntegration, fastifyIntegration, + firebaseIntegration, flush, fsIntegration, functionToStringIntegration, diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts index 7cf8e17f0dd7..b99c481fd1d3 100644 --- a/packages/aws-serverless/src/index.ts +++ b/packages/aws-serverless/src/index.ts @@ -89,6 +89,7 @@ export { connectIntegration, setupConnectErrorHandler, fastifyIntegration, + firebaseIntegration, fsIntegration, genericPoolIntegration, graphqlIntegration, diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index 024e3e3af5e8..b9af910eb0f1 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -107,6 +107,7 @@ export { setupExpressErrorHandler, fastifyIntegration, setupFastifyErrorHandler, + firebaseIntegration, koaIntegration, setupKoaErrorHandler, connectIntegration, diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts index ba6d9640a8b5..8339e95c77a3 100644 --- a/packages/google-cloud-serverless/src/index.ts +++ b/packages/google-cloud-serverless/src/index.ts @@ -90,6 +90,7 @@ export { connectIntegration, setupConnectErrorHandler, fastifyIntegration, + firebaseIntegration, genericPoolIntegration, graphqlIntegration, knexIntegration, From d83a5bded8c6f3ab5eb31027cc07acfae59c48ab Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Thu, 26 Jun 2025 13:14:14 +0100 Subject: [PATCH 04/20] Add JSDoc --- .../tracing/firebase/otel/firebaseInstrumentation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node/src/integrations/tracing/firebase/otel/firebaseInstrumentation.ts b/packages/node/src/integrations/tracing/firebase/otel/firebaseInstrumentation.ts index 5447eeda552f..ad67ea701079 100644 --- a/packages/node/src/integrations/tracing/firebase/otel/firebaseInstrumentation.ts +++ b/packages/node/src/integrations/tracing/firebase/otel/firebaseInstrumentation.ts @@ -7,7 +7,7 @@ const DefaultFirebaseInstrumentationConfig: FirebaseInstrumentationConfig = {}; const firestoreSupportedVersions = ['>=3.0.0 <5']; // firebase 9+ /** - * + * Instrumentation for Firebase services, specifically Firestore. */ export class FirebaseInstrumentation extends InstrumentationBase { public constructor(config: FirebaseInstrumentationConfig = DefaultFirebaseInstrumentationConfig) { From 0d72991916232ed3591b446c360f0f69540cc7ec Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Wed, 16 Jul 2025 15:57:07 +0100 Subject: [PATCH 05/20] Apply suggestions from code review Co-authored-by: Sigrid Huemer <32902192+s1gr1d@users.noreply.github.com> --- .../integrations/tracing/firebase/otel/patches/firestore.ts | 5 ++--- .../node/src/integrations/tracing/firebase/otel/types.ts | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts b/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts index e4fa56cca4a5..2858ed35f6d0 100644 --- a/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts +++ b/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts @@ -51,9 +51,8 @@ export function patchFirestore( config: FirebaseInstrumentationConfig, ): InstrumentationNodeModuleDefinition { // eslint-disable-next-line @typescript-eslint/no-empty-function - const defaultFirestoreSpanCreationHook: FirestoreSpanCreationHook = () => {}; - - let firestoreSpanCreationHook: FirestoreSpanCreationHook = defaultFirestoreSpanCreationHook; + // Setting an empty function as a default + let firestoreSpanCreationHook: FirestoreSpanCreationHook = () => {}; const configFirestoreSpanCreationHook = config.firestoreSpanCreationHook; if (typeof configFirestoreSpanCreationHook === 'function') { diff --git a/packages/node/src/integrations/tracing/firebase/otel/types.ts b/packages/node/src/integrations/tracing/firebase/otel/types.ts index d6efadaa77e7..ecc48bc09498 100644 --- a/packages/node/src/integrations/tracing/firebase/otel/types.ts +++ b/packages/node/src/integrations/tracing/firebase/otel/types.ts @@ -46,7 +46,7 @@ export interface DocumentReference; } From 5adfbdad14bff9c22367157927dfd5399614778a Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Wed, 16 Jul 2025 16:03:01 +0100 Subject: [PATCH 06/20] Fix import paths --- packages/node/src/integrations/tracing/firebase/firebase.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/node/src/integrations/tracing/firebase/firebase.ts b/packages/node/src/integrations/tracing/firebase/firebase.ts index c2e701d24e9a..9f2abbfe31fd 100644 --- a/packages/node/src/integrations/tracing/firebase/firebase.ts +++ b/packages/node/src/integrations/tracing/firebase/firebase.ts @@ -1,8 +1,7 @@ import type { Span } from '@opentelemetry/api'; import type { IntegrationFn } from '@sentry/core'; import { defineIntegration, SEMANTIC_ATTRIBUTE_SENTRY_OP } from '@sentry/core'; -import { generateInstrumentOnce } from '../../../otel/instrument'; -import { addOriginToSpan } from '../../../utils/addOriginToSpan'; +import { addOriginToSpan, generateInstrumentOnce } from '@sentry/node-core'; import { type FirebaseInstrumentationConfig, FirebaseInstrumentation } from './otel'; const INTEGRATION_NAME = 'Firebase'; From 6398ce76d498bd53ba21b78202dd86cfede32a0f Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Wed, 16 Jul 2025 16:03:28 +0100 Subject: [PATCH 07/20] Rename internal `startSpan` to `startDBSpan` --- .../tracing/firebase/otel/patches/firestore.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts b/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts index 2858ed35f6d0..f13b41e5942a 100644 --- a/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts +++ b/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts @@ -155,7 +155,7 @@ function patchAddDoc( reference: CollectionReference, data: WithFieldValue, ): Promise> { - const span = startSpan(tracer, 'addDoc', reference); + const span = startDBSpan(tracer, 'addDoc', reference); firestoreSpanCreationHook(span); return executeContextWithSpan>>(span, () => { return original(reference, data); @@ -172,7 +172,7 @@ function patchDeleteDoc( ) => (this: FirebaseInstrumentation, reference: DocumentReference) => Promise { return function deleteDoc(original: DeleteDocType) { return function patchDeleteDoc(reference: DocumentReference): Promise { - const span = startSpan(tracer, 'deleteDoc', reference.parent || reference); + const span = startDBSpan(tracer, 'deleteDoc', reference.parent || reference); firestoreSpanCreationHook(span); return executeContextWithSpan>(span, () => { return original(reference); @@ -194,7 +194,7 @@ function patchGetDocs( return function patchGetDocs( reference: CollectionReference, ): Promise> { - const span = startSpan(tracer, 'getDocs', reference); + const span = startDBSpan(tracer, 'getDocs', reference); firestoreSpanCreationHook(span); return executeContextWithSpan>>(span, () => { return original(reference); @@ -220,7 +220,7 @@ function patchSetDoc( data: WithFieldValue & PartialWithFieldValue, options?: SetOptions, ): Promise { - const span = startSpan(tracer, 'setDocs', reference.parent || reference); + const span = startDBSpan(tracer, 'setDocs', reference.parent || reference); firestoreSpanCreationHook(span); return executeContextWithSpan>(span, () => { @@ -247,7 +247,7 @@ function executeContextWithSpan(span: Span, callback: () => T): T { }); } -function startSpan( +function startDBSpan( tracer: Tracer, spanName: string, reference: CollectionReference | DocumentReference, From 0d761cd82f943c797cefab98db3f8bffb206fd9c Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Wed, 16 Jul 2025 16:15:13 +0100 Subject: [PATCH 08/20] Lint --- .../src/integrations/tracing/firebase/otel/patches/firestore.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts b/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts index f13b41e5942a..efc043c50dbc 100644 --- a/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts +++ b/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts @@ -50,8 +50,8 @@ export function patchFirestore( unwrap: typeof shimmerUnwrap, config: FirebaseInstrumentationConfig, ): InstrumentationNodeModuleDefinition { - // eslint-disable-next-line @typescript-eslint/no-empty-function // Setting an empty function as a default + // eslint-disable-next-line @typescript-eslint/no-empty-function let firestoreSpanCreationHook: FirestoreSpanCreationHook = () => {}; const configFirestoreSpanCreationHook = config.firestoreSpanCreationHook; From a2bf7065cdf039f5820ff066feff4001e21bdc20 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Fri, 18 Jul 2025 22:45:52 +0100 Subject: [PATCH 09/20] Reimplement e2e test --- .../node-firebase/.firebaserc | 5 + .../node-firebase/createConfigFromEnv.js | 14 -- .../node-firebase/createEnvFromConfig.js | 10 -- .../node-firebase/docker/.gitignore | 6 - .../node-firebase/docker/Dockerfile | 15 -- .../node-firebase/docker/docker-compose.yml | 60 ------- .../node-firebase/docker/firebase/.firebaserc | 5 - .../node-firebase/docker/firebase/.gitignore | 69 -------- .../createOrUpdateConfigIfPossible.js | 3 - .../docker/firebase/database.rules.json | 7 - .../docker/firebase/firestore.rules | 8 - .../docker/firebase/storage.rules | 8 - .../node-firebase/docker/firebase/utils.js | 145 ----------------- .../node-firebase/docker/nginx.conf | 150 ------------------ .../node-firebase/docker/serve.sh | 64 -------- .../{docker/firebase => }/firebase.json | 15 +- .../firebase => }/firestore.indexes.json | 0 .../node-firebase/firestore.rules | 18 +++ .../node-firebase/package.json | 19 ++- .../node-firebase/src/app.ts | 4 +- .../node-firebase/tests/transactions.test.ts | 8 +- .../firebase/otel/patches/firestore.ts | 4 +- 22 files changed, 48 insertions(+), 589 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/node-firebase/.firebaserc delete mode 100644 dev-packages/e2e-tests/test-applications/node-firebase/createConfigFromEnv.js delete mode 100644 dev-packages/e2e-tests/test-applications/node-firebase/createEnvFromConfig.js delete mode 100644 dev-packages/e2e-tests/test-applications/node-firebase/docker/.gitignore delete mode 100644 dev-packages/e2e-tests/test-applications/node-firebase/docker/Dockerfile delete mode 100644 dev-packages/e2e-tests/test-applications/node-firebase/docker/docker-compose.yml delete mode 100644 dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/.firebaserc delete mode 100644 dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/.gitignore delete mode 100644 dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/createOrUpdateConfigIfPossible.js delete mode 100644 dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/database.rules.json delete mode 100644 dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/firestore.rules delete mode 100644 dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/storage.rules delete mode 100644 dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/utils.js delete mode 100644 dev-packages/e2e-tests/test-applications/node-firebase/docker/nginx.conf delete mode 100644 dev-packages/e2e-tests/test-applications/node-firebase/docker/serve.sh rename dev-packages/e2e-tests/test-applications/node-firebase/{docker/firebase => }/firebase.json (55%) rename dev-packages/e2e-tests/test-applications/node-firebase/{docker/firebase => }/firestore.indexes.json (100%) create mode 100644 dev-packages/e2e-tests/test-applications/node-firebase/firestore.rules diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/.firebaserc b/dev-packages/e2e-tests/test-applications/node-firebase/.firebaserc new file mode 100644 index 000000000000..47e4665f6905 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-firebase/.firebaserc @@ -0,0 +1,5 @@ +{ + "projects": { + "default": "sentry-firebase-e2e-test-f4ed3" + } +} diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/createConfigFromEnv.js b/dev-packages/e2e-tests/test-applications/node-firebase/createConfigFromEnv.js deleted file mode 100644 index 3345a9d868fb..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-firebase/createConfigFromEnv.js +++ /dev/null @@ -1,14 +0,0 @@ -const path = require('path'); -const dotent = require('dotenv'); -dotent.config({ path: path.resolve(__dirname, './docker/.env') }); - -const createConfigFromEnv = require('./docker/firebase/utils').createConfigFromEnv; - -// eslint-disable-next-line @typescript-eslint/no-floating-promises -(async () => { - try { - await createConfigFromEnv(); - } catch (e) { - console.error(e); - } -})(); diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/createEnvFromConfig.js b/dev-packages/e2e-tests/test-applications/node-firebase/createEnvFromConfig.js deleted file mode 100644 index d19ea4cb1dc9..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-firebase/createEnvFromConfig.js +++ /dev/null @@ -1,10 +0,0 @@ -const createEnvFromConfig = require('./docker/firebase/utils').createEnvFromConfig; - -(async () => { - try { - await createEnvFromConfig(); - } catch (e) { - console.error(e); - } -})(); - diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/docker/.gitignore b/dev-packages/e2e-tests/test-applications/node-firebase/docker/.gitignore deleted file mode 100644 index b8b9f2089493..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-firebase/docker/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -.config -cache -firebase/data -firebase/firebase-export-* -*-debug.log -.env diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/docker/Dockerfile b/dev-packages/e2e-tests/test-applications/node-firebase/docker/Dockerfile deleted file mode 100644 index ce0fde56dbc7..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-firebase/docker/Dockerfile +++ /dev/null @@ -1,15 +0,0 @@ -FROM node:20-alpine - -ARG FIREBASE_VERSION - -RUN apk --no-cache add openjdk11-jre bash curl openssl gettext nano nginx sudo && \ - npm cache clean --force && \ - npm i -g firebase-tools@$FIREBASE_VERSION - -COPY nginx.conf /etc/nginx/ -COPY serve.sh /usr/bin/ -RUN chmod +x /usr/bin/serve.sh - -WORKDIR /srv/firebase - -ENTRYPOINT ["/usr/bin/serve.sh"] diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/docker/docker-compose.yml b/dev-packages/e2e-tests/test-applications/node-firebase/docker/docker-compose.yml deleted file mode 100644 index 00b019398522..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-firebase/docker/docker-compose.yml +++ /dev/null @@ -1,60 +0,0 @@ -services: - emulator: - container_name: sentry-firebase - build: - context: . - dockerfile: Dockerfile - args: - - FIREBASE_VERSION=13.19.0 - stop_grace_period: 1m - environment: - FIREBASE_AUTH_EMULATOR_HOST: 'localhost:5507' - FIRESTORE_EMULATOR_HOST: 'localhost:5504' - PUBSUB_EMULATOR_HOST: 'localhost:5505' - FUNCTIONS_EMULATOR_HOST: 'localhost:5503' - FIREBASE_PROJECT: 'sentry-15d85' - GCLOUD_PROJECT: 'sentry-15d85' - FORCE_COLOR: 'true' - DATA_DIRECTORY: 'data' - CHOKIDAR_USEPOLLING: 'true' - CONFIG_FIREBASE_TOOLS: ${CONFIG_FIREBASE_TOOLS} - CONFIG_UPDATE_NOTIFIER_FIREBASE_TOOLS: ${CONFIG_UPDATE_NOTIFIER_FIREBASE_TOOLS} - ports: - - '5500:4001' # ui - - '5501:4401' # hub - - '5502:4601' # logging - - '5503:5002' # functions - - '5504:8081' # firestore - - '5505:8086' # pubsub - - '5506:9001' # database - - '5507:9100' # auth - - '5508:9200' # Storage - - '5509:6001' # Hosting - - '5510:9081' # firestore (grpc) - - '5511:9230' # cloud_functions_debug - - '9005:9005' # to be able to authenticate using gmail and docker - volumes: - - type: bind - source: ./firebase - target: /srv/firebase - bind: - create_host_path: true - - type: bind - source: ./cache - target: /root/.cache - bind: - create_host_path: true - - type: bind - source: .config - target: /root/.config - bind: - create_host_path: true - - type: bind - source: ./firebase/data - target: /srv/firebase/data - bind: - create_host_path: true - -networks: - default: - driver: bridge diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/.firebaserc b/dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/.firebaserc deleted file mode 100644 index 392290d3c931..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/.firebaserc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "projects": { - "default": "sentry-project-436908" - } -} diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/.gitignore b/dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/.gitignore deleted file mode 100644 index b17f63107554..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/.gitignore +++ /dev/null @@ -1,69 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -firebase-debug.log* -firebase-debug.*.log* - -# Firebase cache -.firebase/ - -# Firebase config - -# Uncomment this if you'd like others to create their own Firebase project. -# For a team working on the same Firebase project(s), it is recommended to leave -# it commented so all members can deploy to the same project(s) in .firebaserc. -# .firebaserc - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (http://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variables file -.env - -# dataconnect generated files -.dataconnect diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/createOrUpdateConfigIfPossible.js b/dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/createOrUpdateConfigIfPossible.js deleted file mode 100644 index 3ac51191deab..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/createOrUpdateConfigIfPossible.js +++ /dev/null @@ -1,3 +0,0 @@ -const createOrUpdateConfigIfPossible = require('./utils').createOrUpdateConfigIfPossible; - -createOrUpdateConfigIfPossible(); diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/database.rules.json b/dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/database.rules.json deleted file mode 100644 index 4335e236f6db..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/database.rules.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - /* Visit https://firebase.google.com/docs/database/security to learn more about security rules. */ - "rules": { - ".read": false, - ".write": false - } -} diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/firestore.rules b/dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/firestore.rules deleted file mode 100644 index c3c1733dcd8b..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/firestore.rules +++ /dev/null @@ -1,8 +0,0 @@ -rules_version = '2'; -service cloud.firestore { - match /databases/{database}/documents { - match /{document=**} { - allow read, write: if true; - } - } -} diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/storage.rules b/dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/storage.rules deleted file mode 100644 index 4cc806e7d390..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/storage.rules +++ /dev/null @@ -1,8 +0,0 @@ -rules_version = '2'; -service firebase.storage { - match /b/{bucket}/o { - match /{file=**} { - allow read, write: if true; - } - } -} diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/utils.js b/dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/utils.js deleted file mode 100644 index 0fc6069c682e..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/utils.js +++ /dev/null @@ -1,145 +0,0 @@ -const fs = require('fs'); -const path = require('path'); - -const firebaseToolsFileName = 'firebase-tools.json'; -const updateNotifierFirebaseToolsFileName = 'update-notifier-firebase-tools.json'; - -function createJsonFile(filePath, json) { - return new Promise((resolve, reject) => { - let content = JSON.stringify(json, null, 2); - - // replace spaces with tabs - content = content.replace(/[ ]{2}/g, '\t'); - - fs.mkdirSync(filePath.substring(0, filePath.lastIndexOf('/')), { recursive: true }); - fs.writeFile(filePath, content, function (err) { - if (err) { - reject(err); - } else { - resolve(); - } - }); - }); -} - -function createEnvironmentFile(filePath, content) { - return new Promise((resolve, reject) => { - fs.writeFile(filePath, content, function (err) { - if (err) { - reject(err); - } else { - resolve(); - } - }); - }); -} - -function readJsonFromFile(filePath) { - return new Promise((resolve, reject) => { - fs.readFile(filePath, 'utf8', (err, data) => { - if (err) { - reject(err); - return; - } - try { - resolve(JSON.parse(data)); - } catch (err) { - reject(err); - console.error(err); - } - }); - }); -} - -/** - * Creates firebase configuration based on .env file - */ -async function createConfigFromEnv() { - const dockerPath = path.resolve(__dirname, '..'); - const configPath = path.resolve(dockerPath, '.config/configstore'); - - let filePathFirebaseTools = process.env.CONFIG_FIREBASE_TOOLS; - let filePathUpdateNotifierFirebaseTools = process.env.CONFIG_UPDATE_NOTIFIER_FIREBASE_TOOLS; - - if (typeof filePathFirebaseTools !== 'string') { - throw new Error('no CONFIG_FIREBASE_TOOLS environment'); - } - if (typeof filePathUpdateNotifierFirebaseTools !== 'string') { - throw new Error('no CONFIG_UPDATE_NOTIFIER_FIREBASE_TOOLS environment'); - } - - try { - filePathFirebaseTools = JSON.parse(filePathFirebaseTools); - filePathUpdateNotifierFirebaseTools = JSON.parse(filePathUpdateNotifierFirebaseTools); - - await Promise.all([ - createJsonFile(path.resolve(configPath, firebaseToolsFileName), filePathFirebaseTools), - createJsonFile( - path.resolve(configPath, updateNotifierFirebaseToolsFileName), - filePathUpdateNotifierFirebaseTools, - ), - ]); - console.log('firebase config based on environment variables created successfully'); - } catch (e) { - console.error('firebase config creation error', e); - } -} - -/** - * Creates file .env based on firebase configuration - */ -async function createEnvFromConfig() { - const dockerPath = path.resolve(__dirname, '..'); - const configPath = path.resolve(dockerPath, '.config/configstore'); - const dockerFilePath = path.resolve(dockerPath, '.env'); - try { - const results = await Promise.all([ - readJsonFromFile(path.resolve(configPath, firebaseToolsFileName)), - readJsonFromFile(path.resolve(configPath, updateNotifierFirebaseToolsFileName)), - ]); - const filePathFirebaseTools = results[0]; - const filePathUpdateNotifierFirebaseTools = results[1]; - - if (!filePathFirebaseTools) { - throw new Error('Environment variable '); - } - - const content = []; - content.push('# This is autogenerated'); - content.push(`CONFIG_FIREBASE_TOOLS=${JSON.stringify(filePathFirebaseTools)}`); - content.push(`CONFIG_UPDATE_NOTIFIER_FIREBASE_TOOLS=${JSON.stringify(filePathUpdateNotifierFirebaseTools)}`); - content.push(''); - await createEnvironmentFile(dockerFilePath, content.join('\n')); - - console.log(`environments variables saved in "${dockerFilePath}" based on firebase config`); - } catch (e) { - console.error(e); - } -} - -/** - * Creates or update the existing config whenever environment is defined. This is used by docker and will recreate - * the config each time the docker is run as long as the environment settings exist. - */ -function createOrUpdateConfigIfPossible() { - const filePathFirebaseTools = process.env.CONFIG_FIREBASE_TOOLS; - const filePathUpdateNotifierFirebaseTools = process.env.CONFIG_UPDATE_NOTIFIER_FIREBASE_TOOLS; - if ( - typeof filePathFirebaseTools === 'string' && - typeof filePathUpdateNotifierFirebaseTools === 'string' && - filePathFirebaseTools !== '' && - filePathUpdateNotifierFirebaseTools !== '' - ) { - createConfigFromEnv(); - } else { - console.error('>>>>>>>>>>>>>>>> WARNING <<<<<<<<<<<<<<<<<<<<<'); - console.error('firebase config creation failed due to missing environment variables'); - console.error('>>>>>>>>>>>>>>>> WARNING <<<<<<<<<<<<<<<<<<<<<'); - } -} - -module.exports = { - createEnvFromConfig: createEnvFromConfig, - createConfigFromEnv: createConfigFromEnv, - createOrUpdateConfigIfPossible: createOrUpdateConfigIfPossible, -}; diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/docker/nginx.conf b/dev-packages/e2e-tests/test-applications/node-firebase/docker/nginx.conf deleted file mode 100644 index d8189878c99a..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-firebase/docker/nginx.conf +++ /dev/null @@ -1,150 +0,0 @@ -daemon off; -worker_processes 1; - -# error_log /dev/stderr; - -events { - worker_connections 1024; -} - -http { - client_max_body_size 100M; - - map $http_upgrade $connection_upgrade { - default upgrade; '' close; - } - - server { - listen 0.0.0.0:4001; - server_name ui; - location / { - proxy_pass http://127.0.0.1:5500; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - proxy_set_header Host $host; - } - } - - server { - listen 0.0.0.0:4401; - server_name hub; - location / { - proxy_pass http://127.0.0.1:5501; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - proxy_set_header Host $host; - } - } - - server { - listen 0.0.0.0:4601; - server_name logging; - location / { - proxy_pass http://127.0.0.1:5502; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - proxy_set_header Host $host; - } - } - - server { - listen 0.0.0.0:5002; - server_name functions; - location / { - proxy_pass http://127.0.0.1:5503; - } - } - - server { - listen 0.0.0.0:8081; - server_name firestore; - location / { - proxy_pass http://127.0.0.1:5504; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - proxy_set_header Host $host; - } - } - - server { - listen 0.0.0.0:8086; - http2 on; - server_name pubsub; - location / { - grpc_pass grpc://127.0.0.1:5505; - } - } - - server { - listen 0.0.0.0:9001; - server_name database; - location / { - proxy_pass http://127.0.0.1:5506; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - proxy_set_header Host $host; - } - } - - server { - listen 0.0.0.0:9100; - server_name auth; - location / { - proxy_pass http://127.0.0.1:5507; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - proxy_set_header Host $host; - } - } - - server { - listen 0.0.0.0:9200; - server_name cloud_storage; - location / { - proxy_pass http://127.0.0.1:5508; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - proxy_set_header Host $host; - } - } - - server { - listen 0.0.0.0:6001; - server_name hosting; - location / { - proxy_pass http://127.0.0.1:5509; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - proxy_set_header Host $host; - } - } - - server { - listen 0.0.0.0:9081; - http2 on; - server_name firestore; - location / { - grpc_pass grpc://127.0.0.1:5510; - } - } - - server { - listen 0.0.0.0:9230; - server_name cloud_functions_debug; - location / { - proxy_pass http://127.0.0.1:5511; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - proxy_set_header Host $host; - } - } -} diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/docker/serve.sh b/dev-packages/e2e-tests/test-applications/node-firebase/docker/serve.sh deleted file mode 100644 index 23503e69109c..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-firebase/docker/serve.sh +++ /dev/null @@ -1,64 +0,0 @@ -#!/bin/bash -set -eo pipefail - -# Function to print error messages and exit -error_exit() { - echo "$1" 1>&2 - exit 1 -} - -# Sanity checks -[[ -z "${DATA_DIRECTORY}" ]] && echo "DATA_DIRECTORY environment variable missing, will not export or import data to firebase" -[[ -z "${FIREBASE_PROJECT}" ]] && error_exit "FIREBASE_PROJECT environment variable missing" - -dirs=("/srv/firebase/functions" "/srv/firebase/firestore" "/srv/firebase/storage") - -for dir in "${dirs[@]}"; do - if [[ -d "$dir" ]]; then - echo "Installing npm packages in $dir" - npm install --prefix "$dir" || error_exit "npm install failed in $dir" - fi -done - -if [[ -z "${CONFIG_FIREBASE_TOOLS}" ]];then - echo "CONFIG_FIREBASE_TOOLS environment variable missing" -fi -if [[ -z "${CONFIG_UPDATE_NOTIFIER_FIREBASE_TOOLS}" ]];then - echo "CONFIG_UPDATE_NOTIFIER_FIREBASE_TOOLS environment variable missing" -fi - -node createOrUpdateConfigIfPossible.js -sleep 1 - -# Start Firebase emulators -emulator_cmd="firebase emulators:start --project=${FIREBASE_PROJECT}" -[[ -n "${DATA_DIRECTORY}" ]] && emulator_cmd+=" --import=./${DATA_DIRECTORY}/export --export-on-exit" -$emulator_cmd & -firebase_pid=$! - -# Start nginx and npm -echo "Starting nginx..." -nginx & -nginx_pid=$! - -cleanup() { - echo "Stopping services..." - # Gracefully stop background processes - echo "Terminating background services..." - if [[ -n "$firebase_pid" ]]; then - kill -SIGTERM "$firebase_pid" || echo "Failed to terminate Firebase process" - wait "$firebase_pid" 2>/dev/null - fi - if [[ -n "$nginx_pid" ]]; then - kill -SIGTERM "$nginx_pid" || echo "Failed to terminate Nginx process" - wait "$nginx_pid" 2>/dev/null - fi - if [[ -n "$npm_pid" ]]; then - kill -SIGTERM "$npm_pid" || echo "Failed to terminate NPM process" - wait "$npm_pid" 2>/dev/null - fi -} - -trap cleanup INT TERM SIGTERM SIGINT - -wait diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/firebase.json b/dev-packages/e2e-tests/test-applications/node-firebase/firebase.json similarity index 55% rename from dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/firebase.json rename to dev-packages/e2e-tests/test-applications/node-firebase/firebase.json index 714e003f7324..05203f1d6567 100644 --- a/dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/firebase.json +++ b/dev-packages/e2e-tests/test-applications/node-firebase/firebase.json @@ -1,24 +1,19 @@ { - "database": { - "rules": "database.rules.json" - }, "firestore": { + "database": "(default)", + "location": "nam5", "rules": "firestore.rules", "indexes": "firestore.indexes.json" }, "emulators": { - "auth": { - "port": 5507 - }, "firestore": { - "port": 5504 + "port": 8080 }, "database": { - "port": 5506 + "port": 9000 }, "ui": { - "enabled": true, - "port": 5500 + "enabled": true }, "singleProjectMode": true } diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/firestore.indexes.json b/dev-packages/e2e-tests/test-applications/node-firebase/firestore.indexes.json similarity index 100% rename from dev-packages/e2e-tests/test-applications/node-firebase/docker/firebase/firestore.indexes.json rename to dev-packages/e2e-tests/test-applications/node-firebase/firestore.indexes.json diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/firestore.rules b/dev-packages/e2e-tests/test-applications/node-firebase/firestore.rules new file mode 100644 index 000000000000..260e089a299b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-firebase/firestore.rules @@ -0,0 +1,18 @@ +rules_version='2' + +service cloud.firestore { + match /databases/{database}/documents { + match /{document=**} { + // This rule allows anyone with your database reference to view, edit, + // and delete all data in your database. It is useful for getting + // started, but it is configured to expire after 30 days because it + // leaves your app open to attackers. At that time, all client + // requests to your database will be denied. + // + // Make sure to write security rules for your app before that time, or + // else all client requests to your database will be denied until you + // update your rules. + allow read, write: if request.time < timestamp.date(2025, 8, 17); + } + } +} 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 6d1e93d917f8..0a23fbbeef92 100644 --- a/dev-packages/e2e-tests/test-applications/node-firebase/package.json +++ b/dev-packages/e2e-tests/test-applications/node-firebase/package.json @@ -4,30 +4,33 @@ "private": true, "scripts": { "build": "tsc", - "createEnvFromConfig": "node createEnvFromConfig.js", - "createConfigFromEnv": "node createConfigFromEnv.js", "dev": "tsc --build --watch", - "docker": "cd docker && docker compose up --build -d", "proxy": "node start-event-proxy.mjs", + "emulate": "firebase emulators:start &", "start": "node ./dist/app.js", "test": "playwright test", "clean": "npx rimraf node_modules pnpm-lock.yaml", - "test:build": "pnpm install && pnpm docker && pnpm build", - "test:assert": "pnpm test" + "test:build": "pnpm install && pnpm build", + "test:assert": "pnpm firebase emulators:exec 'pnpm test'" }, "dependencies": { "@firebase/app": "^0.13.1", "@sentry/node": "latest || *", "@sentry/core": "latest || *", "@sentry/opentelemetry": "latest || *", + "@types/node": "^18.19.1", "dotenv": "^16.4.5", - "firebase": "^11.0.1", + "express": "^4.18.2", + "firebase": "^12.0.0", + "firebase-admin": "^12.0.0", "tsconfig-paths": "^4.2.0", "typescript": "4.9.5" }, "devDependencies": { - "@playwright/test": "~1.50.0", - "@sentry-internal/test-utils": "link:../../../test-utils" + "@playwright/test": "~1.53.2", + "@sentry-internal/test-utils": "link:../../../test-utils", + "@types/express": "^4.17.13", + "firebase-tools": "^12.0.0" }, "volta": { "extends": "../../package.json" diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/src/app.ts b/dev-packages/e2e-tests/test-applications/node-firebase/src/app.ts index be7a18b7bca6..486aa06b5ffc 100644 --- a/dev-packages/e2e-tests/test-applications/node-firebase/src/app.ts +++ b/dev-packages/e2e-tests/test-applications/node-firebase/src/app.ts @@ -12,7 +12,7 @@ import { getDocs, getFirestore, setDoc, -} from 'firebase/firestore/lite'; // seems like "firebase/firestore" is trying to use grpc for connection and it +} from 'firebase/firestore/lite'; const options: FirebaseOptions = { projectId: 'sentry-15d85', @@ -22,7 +22,7 @@ const options: FirebaseOptions = { const app = initializeApp(options); const db = getFirestore(app); -connectFirestoreEmulator(db, '127.0.0.1', 5504); +connectFirestoreEmulator(db, '127.0.0.1', 8080); const citiesRef = collection(db, 'cities'); async function addCity(): Promise { diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-firebase/tests/transactions.test.ts index 879606105a13..1fcb2c8047f5 100644 --- a/dev-packages/e2e-tests/test-applications/node-firebase/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-firebase/tests/transactions.test.ts @@ -12,7 +12,7 @@ const spanAddDoc = expect.objectContaining({ 'firebase.firestore.type': 'collection', 'otel.kind': 'CLIENT', 'server.address': '127.0.0.1', - 'server.port': '5504', + 'server.port': '8080', 'sentry.origin': 'auto.firebase.otel.firestore', 'sentry.op': 'db.query', }), @@ -37,7 +37,7 @@ const spanSetDocs = expect.objectContaining({ 'firebase.firestore.type': 'collection', 'otel.kind': 'CLIENT', 'server.address': '127.0.0.1', - 'server.port': '5504', + 'server.port': '8080', 'sentry.origin': 'auto.firebase.otel.firestore', 'sentry.op': 'db.query', }), @@ -62,7 +62,7 @@ const spanGetDocs = expect.objectContaining({ 'firebase.firestore.type': 'collection', 'otel.kind': 'CLIENT', 'server.address': '127.0.0.1', - 'server.port': '5504', + 'server.port': '8080', 'sentry.origin': 'auto.firebase.otel.firestore', 'sentry.op': 'db.query', }), @@ -87,7 +87,7 @@ const spanDeleteDoc = expect.objectContaining({ 'firebase.firestore.type': 'collection', 'otel.kind': 'CLIENT', 'server.address': '127.0.0.1', - 'server.port': '5504', + 'server.port': '8080', 'sentry.origin': 'auto.firebase.otel.firestore', 'sentry.op': 'db.query', }), diff --git a/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts b/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts index efc043c50dbc..d111b8581205 100644 --- a/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts +++ b/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts @@ -63,7 +63,7 @@ export function patchFirestore( if (!error) { return; } - diag.error(error?.message); + diag.error('Firebase Firestore span creation hook failed', error?.message); }, true, ); @@ -156,7 +156,9 @@ function patchAddDoc( data: WithFieldValue, ): Promise> { const span = startDBSpan(tracer, 'addDoc', reference); + firestoreSpanCreationHook(span); + return executeContextWithSpan>>(span, () => { return original(reference, data); }); From f3c5ca35c32cf98e8e472f7c19853d38f0ee1daa Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Sat, 19 Jul 2025 13:32:28 +0100 Subject: [PATCH 10/20] Simplify patch logic --- .../tracing/firebase/otel/patches/firestore.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts b/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts index d111b8581205..f13362995ae4 100644 --- a/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts +++ b/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts @@ -50,9 +50,10 @@ export function patchFirestore( unwrap: typeof shimmerUnwrap, config: FirebaseInstrumentationConfig, ): InstrumentationNodeModuleDefinition { - // Setting an empty function as a default // eslint-disable-next-line @typescript-eslint/no-empty-function - let firestoreSpanCreationHook: FirestoreSpanCreationHook = () => {}; + const defaultFirestoreSpanCreationHook: FirestoreSpanCreationHook = () => {}; + + let firestoreSpanCreationHook: FirestoreSpanCreationHook = defaultFirestoreSpanCreationHook; const configFirestoreSpanCreationHook = config.firestoreSpanCreationHook; if (typeof configFirestoreSpanCreationHook === 'function') { @@ -63,7 +64,7 @@ export function patchFirestore( if (!error) { return; } - diag.error('Firebase Firestore span creation hook failed', error?.message); + diag.error(error?.message); }, true, ); @@ -151,14 +152,12 @@ function patchAddDoc( data: WithFieldValue, ) => Promise> { return function addDoc(original: AddDocType) { - return function patchAddDoc( + return function ( reference: CollectionReference, data: WithFieldValue, ): Promise> { const span = startDBSpan(tracer, 'addDoc', reference); - firestoreSpanCreationHook(span); - return executeContextWithSpan>>(span, () => { return original(reference, data); }); @@ -173,7 +172,7 @@ function patchDeleteDoc( original: DeleteDocType, ) => (this: FirebaseInstrumentation, reference: DocumentReference) => Promise { return function deleteDoc(original: DeleteDocType) { - return function patchDeleteDoc(reference: DocumentReference): Promise { + return function (reference: DocumentReference): Promise { const span = startDBSpan(tracer, 'deleteDoc', reference.parent || reference); firestoreSpanCreationHook(span); return executeContextWithSpan>(span, () => { @@ -193,7 +192,7 @@ function patchGetDocs( reference: CollectionReference, ) => Promise> { return function getDocs(original: GetDocsType) { - return function patchGetDocs( + return function ( reference: CollectionReference, ): Promise> { const span = startDBSpan(tracer, 'getDocs', reference); @@ -217,7 +216,7 @@ function patchSetDoc( options?: SetOptions, ) => Promise { return function setDoc(original: SetDocType) { - return function patchSetDoc( + return function ( reference: DocumentReference, data: WithFieldValue & PartialWithFieldValue, options?: SetOptions, From eddeb8ec74ae53ad415b5e11f81df3954d0b6f86 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Mon, 21 Jul 2025 10:42:40 +0100 Subject: [PATCH 11/20] Add `deleteDoc` unwrapper --- .../integrations/tracing/firebase/otel/patches/firestore.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts b/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts index f13362995ae4..30588c9f4580 100644 --- a/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts +++ b/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts @@ -138,6 +138,11 @@ function unwrapMethods( unwrap(moduleExports, 'setDoc'); } + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + if (isWrapped(moduleExports.deleteDoc)) { + unwrap(moduleExports, 'deleteDoc'); + } + return moduleExports; } From cab996e39513a383bc6c91d721392f66522cc65e Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Mon, 21 Jul 2025 11:05:05 +0100 Subject: [PATCH 12/20] Simplify unwrappers --- .../firebase/otel/patches/firestore.ts | 25 +++++-------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts b/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts index 30588c9f4580..26023de146e9 100644 --- a/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts +++ b/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts @@ -123,26 +123,13 @@ function unwrapMethods( unwrap: typeof shimmerUnwrap, // eslint-disable-next-line @typescript-eslint/no-explicit-any ): any { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - if (isWrapped(moduleExports.addDoc)) { - unwrap(moduleExports, 'addDoc'); - } - - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - if (isWrapped(moduleExports.getDocs)) { - unwrap(moduleExports, 'getDocs'); - } - - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - if (isWrapped(moduleExports.setDoc)) { - unwrap(moduleExports, 'setDoc'); - } - - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - if (isWrapped(moduleExports.deleteDoc)) { - unwrap(moduleExports, 'deleteDoc'); + for (const method of ['addDoc', 'getDocs', 'setDoc', 'deleteDoc']) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + if (isWrapped(moduleExports[method])) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + unwrap(moduleExports, method); + } } - return moduleExports; } From d30be51ad40aaed755757fe62eddd7b618915e5b Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Mon, 21 Jul 2025 12:53:08 +0100 Subject: [PATCH 13/20] Correct `setDoc` span name --- .../node-firebase/tests/transactions.test.ts | 4 ++-- .../integrations/tracing/firebase/otel/patches/firestore.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-firebase/tests/transactions.test.ts index 1fcb2c8047f5..4e0e93b3c933 100644 --- a/dev-packages/e2e-tests/test-applications/node-firebase/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-firebase/tests/transactions.test.ts @@ -27,11 +27,11 @@ const spanAddDoc = expect.objectContaining({ }); const spanSetDocs = expect.objectContaining({ - description: 'setDocs cities', + description: 'setDoc cities', data: expect.objectContaining({ 'db.collection.name': 'cities', 'db.namespace': '[DEFAULT]', - 'db.operation.name': 'setDocs', + 'db.operation.name': 'setDoc', 'db.system.name': 'firebase.firestore', 'firebase.firestore.options.projectId': 'sentry-15d85', 'firebase.firestore.type': 'collection', diff --git a/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts b/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts index 26023de146e9..ac10ddc357bf 100644 --- a/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts +++ b/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts @@ -213,7 +213,7 @@ function patchSetDoc( data: WithFieldValue & PartialWithFieldValue, options?: SetOptions, ): Promise { - const span = startDBSpan(tracer, 'setDocs', reference.parent || reference); + const span = startDBSpan(tracer, 'setDoc', reference.parent || reference); firestoreSpanCreationHook(span); return executeContextWithSpan>(span, () => { From d27149e764f3447878295d59cb3576ced9bc8fd0 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Mon, 21 Jul 2025 16:34:10 +0100 Subject: [PATCH 14/20] Increase size limit --- .size-limit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.size-limit.js b/.size-limit.js index 13b963bacd8d..058f298c02f3 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -233,7 +233,7 @@ module.exports = [ import: createImport('init'), ignore: [...builtinModules, ...nodePrefixedBuiltinModules], gzip: true, - limit: '144 KB', + limit: '145 KB', }, { name: '@sentry/node - without tracing', From 3636b820c60746b7a6e62b458ab4ba7d0c78c0aa Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Mon, 21 Jul 2025 21:00:08 +0100 Subject: [PATCH 15/20] Handle IPv6 hosts --- .../tracing/firebase/otel/patches/firestore.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts b/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts index ac10ddc357bf..ebb7491ed884 100644 --- a/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts +++ b/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts @@ -1,3 +1,4 @@ +import * as net from 'node:net'; import type { Span, Tracer } from '@opentelemetry/api'; import { context, diag, SpanKind, trace } from '@opentelemetry/api'; import { @@ -272,10 +273,13 @@ function addAttributes( }; if (typeof settings.host === 'string') { - const arr = settings.host.split(':'); + const arr = net.isIPv6(settings.host) + ? settings.host.split(']:') // Handling IPv6 addresses + : settings.host.split(':'); // Handling IPv4 addresses + if (arr.length === 2) { attributes[ATTR_SERVER_ADDRESS] = arr[0]; - attributes[ATTR_SERVER_PORT] = arr[1]; + attributes[ATTR_SERVER_PORT] = Number(arr[1]); } } From ed24b877a04c6d61a8d24b1009b4e13a30273e72 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Mon, 21 Jul 2025 21:54:29 +0100 Subject: [PATCH 16/20] Handle IPv6 hosts better --- .../node-firebase/tests/transactions.test.ts | 8 +++---- .../firebase/otel/patches/firestore.ts | 23 +++++++++++++------ 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-firebase/tests/transactions.test.ts index 4e0e93b3c933..749d818aee66 100644 --- a/dev-packages/e2e-tests/test-applications/node-firebase/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-firebase/tests/transactions.test.ts @@ -12,7 +12,7 @@ const spanAddDoc = expect.objectContaining({ 'firebase.firestore.type': 'collection', 'otel.kind': 'CLIENT', 'server.address': '127.0.0.1', - 'server.port': '8080', + 'server.port': 8080, 'sentry.origin': 'auto.firebase.otel.firestore', 'sentry.op': 'db.query', }), @@ -37,7 +37,7 @@ const spanSetDocs = expect.objectContaining({ 'firebase.firestore.type': 'collection', 'otel.kind': 'CLIENT', 'server.address': '127.0.0.1', - 'server.port': '8080', + 'server.port': 8080, 'sentry.origin': 'auto.firebase.otel.firestore', 'sentry.op': 'db.query', }), @@ -62,7 +62,7 @@ const spanGetDocs = expect.objectContaining({ 'firebase.firestore.type': 'collection', 'otel.kind': 'CLIENT', 'server.address': '127.0.0.1', - 'server.port': '8080', + 'server.port': 8080, 'sentry.origin': 'auto.firebase.otel.firestore', 'sentry.op': 'db.query', }), @@ -87,7 +87,7 @@ const spanDeleteDoc = expect.objectContaining({ 'firebase.firestore.type': 'collection', 'otel.kind': 'CLIENT', 'server.address': '127.0.0.1', - 'server.port': '8080', + 'server.port': 8080, 'sentry.origin': 'auto.firebase.otel.firestore', 'sentry.op': 'db.query', }), diff --git a/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts b/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts index ebb7491ed884..2a46d9adf018 100644 --- a/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts +++ b/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts @@ -1,4 +1,3 @@ -import * as net from 'node:net'; import type { Span, Tracer } from '@opentelemetry/api'; import { context, diag, SpanKind, trace } from '@opentelemetry/api'; import { @@ -273,13 +272,23 @@ function addAttributes( }; if (typeof settings.host === 'string') { - const arr = net.isIPv6(settings.host) - ? settings.host.split(']:') // Handling IPv6 addresses - : settings.host.split(':'); // Handling IPv4 addresses + if (settings.host.startsWith('[') && settings.host.endsWith(']')) { + // Handling IPv6 addresses + attributes[ATTR_SERVER_ADDRESS] = settings.host.slice(1, -1); + } else { + // Handling IPv4 addresses + attributes[ATTR_SERVER_ADDRESS] = settings.host; + } - if (arr.length === 2) { - attributes[ATTR_SERVER_ADDRESS] = arr[0]; - attributes[ATTR_SERVER_PORT] = Number(arr[1]); + if (settings.host.includes(':')) { + // Split the host by ':' to get the address and port + // This will handle both IPv4 and IPv6 addresses correctly + // It will split at the last colon + const lastColonIndex = settings.host.lastIndexOf(':'); + if (lastColonIndex !== -1) { + attributes[ATTR_SERVER_ADDRESS] = settings.host.slice(0, lastColonIndex); + attributes[ATTR_SERVER_PORT] = Number(settings.host.slice(lastColonIndex + 1)); + } } } From 14160171c71805eee9962e72e8d833c2ade40040 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Tue, 22 Jul 2025 10:27:38 +0100 Subject: [PATCH 17/20] Handle IPv6 hosts even better --- .../firebase/otel/patches/firestore.ts | 49 +++++++++++++------ 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts b/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts index 2a46d9adf018..7545ea2b888e 100644 --- a/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts +++ b/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts @@ -272,23 +272,44 @@ function addAttributes( }; if (typeof settings.host === 'string') { - if (settings.host.startsWith('[') && settings.host.endsWith(']')) { - // Handling IPv6 addresses - attributes[ATTR_SERVER_ADDRESS] = settings.host.slice(1, -1); + let address: string | undefined; + let port: string | undefined; + + if (settings.host.startsWith('[')) { + if (settings.host.endsWith(']')) { + // Theres no port, just the address + address = settings.host.slice(1, -1); + } else { + // Handling IPv6 addresses with port + const lastColonIndex = settings.host.lastIndexOf(':'); + if (lastColonIndex !== -1) { + address = settings.host.slice(1, lastColonIndex); + port = settings.host.slice(lastColonIndex + 1); + } + } } else { - // Handling IPv4 addresses - attributes[ATTR_SERVER_ADDRESS] = settings.host; + if (settings.host.includes('::')) { + // Handling IPv6 addresses with port + const parts = settings.host.split(':'); + address = parts.slice(0, -1).join(':'); + port = parts[parts.length - 1]; + } else if (settings.host.includes(':')) { + // Handling IPv4 addresses with port + const parts = settings.host.split(':'); + address = parts[0]; + port = parts[1]; + } else { + // Handling IPv4 addresses without port + address = settings.host; + } } - if (settings.host.includes(':')) { - // Split the host by ':' to get the address and port - // This will handle both IPv4 and IPv6 addresses correctly - // It will split at the last colon - const lastColonIndex = settings.host.lastIndexOf(':'); - if (lastColonIndex !== -1) { - attributes[ATTR_SERVER_ADDRESS] = settings.host.slice(0, lastColonIndex); - attributes[ATTR_SERVER_PORT] = Number(settings.host.slice(lastColonIndex + 1)); - } + if (address) { + attributes[ATTR_SERVER_ADDRESS] = address; + } + + if (port !== undefined) { + attributes[ATTR_SERVER_PORT] = Number(port); } } From 3a2a443f98c313144ce59259ae50199d34db8188 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Tue, 22 Jul 2025 12:53:27 +0100 Subject: [PATCH 18/20] Try to make it more robust --- .../firebase/otel/patches/firestore.ts | 85 +++++++++++-------- 1 file changed, 48 insertions(+), 37 deletions(-) diff --git a/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts b/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts index 7545ea2b888e..df6b58106b67 100644 --- a/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts +++ b/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts @@ -1,3 +1,4 @@ +import * as net from 'node:net'; import type { Span, Tracer } from '@opentelemetry/api'; import { context, diag, SpanKind, trace } from '@opentelemetry/api'; import { @@ -251,36 +252,24 @@ function startDBSpan( return span; } -function addAttributes( - span: Span, - reference: CollectionReference | DocumentReference, -): void { - const firestoreApp: FirebaseApp = reference.firestore.app; - const firestoreOptions: FirebaseOptions = firestoreApp.options; - const json: { settings?: FirestoreSettings } = reference.firestore.toJSON() || {}; - const settings: FirestoreSettings = json.settings || {}; - - const attributes: SpanAttributes = { - [ATTR_DB_COLLECTION_NAME]: reference.path, - [ATTR_DB_NAMESPACE]: firestoreApp.name, - [ATTR_DB_SYSTEM_NAME]: 'firebase.firestore', - 'firebase.firestore.type': reference.type, - 'firebase.firestore.options.projectId': firestoreOptions.projectId, - 'firebase.firestore.options.appId': firestoreOptions.appId, - 'firebase.firestore.options.messagingSenderId': firestoreOptions.messagingSenderId, - 'firebase.firestore.options.storageBucket': firestoreOptions.storageBucket, - }; - +/** + * Sets the server address and port attributes on the span based on the Firestore settings. + * It's best effort to extract the address and port from the settings, especially for IPv6. + * @param span - The span to set attributes on. + * @param settings - The Firestore settings containing host information. + */ +function setPortAndAddress(span: Span, settings: FirestoreSettings): void { if (typeof settings.host === 'string') { let address: string | undefined; let port: string | undefined; if (settings.host.startsWith('[')) { + // IPv6 addresses can be enclosed in square brackets, e.g., [2001:db8::1]:8080 if (settings.host.endsWith(']')) { - // Theres no port, just the address + // IPv6 with square brackets without port address = settings.host.slice(1, -1); } else { - // Handling IPv6 addresses with port + // IPv6 with square brackets with port const lastColonIndex = settings.host.lastIndexOf(':'); if (lastColonIndex !== -1) { address = settings.host.slice(1, lastColonIndex); @@ -288,30 +277,52 @@ function addAttributes( } } } else { - if (settings.host.includes('::')) { - // Handling IPv6 addresses with port - const parts = settings.host.split(':'); - address = parts.slice(0, -1).join(':'); - port = parts[parts.length - 1]; - } else if (settings.host.includes(':')) { - // Handling IPv4 addresses with port - const parts = settings.host.split(':'); - address = parts[0]; - port = parts[1]; - } else { - // Handling IPv4 addresses without port + // IPv4 or IPv6 without square brackets + // If it's an IPv6 address without square brackets, we assume it does not have a port. + if (net.isIPv6(settings.host)) { address = settings.host; } + // If it's an IPv4 address, we can extract the port if it exists. + else { + const lastColonIndex = settings.host.lastIndexOf(':'); + if (lastColonIndex !== -1) { + address = settings.host.slice(0, lastColonIndex); + port = settings.host.slice(lastColonIndex + 1); + } else { + address = settings.host; + } + } } - if (address) { - attributes[ATTR_SERVER_ADDRESS] = address; + span.setAttribute(ATTR_SERVER_ADDRESS, address); } if (port !== undefined) { - attributes[ATTR_SERVER_PORT] = Number(port); + span.setAttribute(ATTR_SERVER_PORT, Number(port)); } } +} + +function addAttributes( + span: Span, + reference: CollectionReference | DocumentReference, +): void { + const firestoreApp: FirebaseApp = reference.firestore.app; + const firestoreOptions: FirebaseOptions = firestoreApp.options; + const json: { settings?: FirestoreSettings } = reference.firestore.toJSON() || {}; + const settings: FirestoreSettings = json.settings || {}; + + const attributes: SpanAttributes = { + [ATTR_DB_COLLECTION_NAME]: reference.path, + [ATTR_DB_NAMESPACE]: firestoreApp.name, + [ATTR_DB_SYSTEM_NAME]: 'firebase.firestore', + 'firebase.firestore.type': reference.type, + 'firebase.firestore.options.projectId': firestoreOptions.projectId, + 'firebase.firestore.options.appId': firestoreOptions.appId, + 'firebase.firestore.options.messagingSenderId': firestoreOptions.messagingSenderId, + 'firebase.firestore.options.storageBucket': firestoreOptions.storageBucket, + }; span.setAttributes(attributes); + setPortAndAddress(span, settings); } From 948db0c41928e1394584157144de52b535a92d96 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Tue, 22 Jul 2025 13:03:03 +0100 Subject: [PATCH 19/20] Brackets --- .../integrations/tracing/firebase/otel/patches/firestore.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts b/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts index df6b58106b67..e8c4a99bbf71 100644 --- a/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts +++ b/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts @@ -267,12 +267,12 @@ function setPortAndAddress(span: Span, settings: FirestoreSettings): void { // IPv6 addresses can be enclosed in square brackets, e.g., [2001:db8::1]:8080 if (settings.host.endsWith(']')) { // IPv6 with square brackets without port - address = settings.host.slice(1, -1); + address = settings.host.replace(/^\[|\]$/g, ''); } else { // IPv6 with square brackets with port const lastColonIndex = settings.host.lastIndexOf(':'); if (lastColonIndex !== -1) { - address = settings.host.slice(1, lastColonIndex); + address = settings.host.slice(1, lastColonIndex).replace(/^\[|\]$/g, ''); port = settings.host.slice(lastColonIndex + 1); } } From 89774abec9e8bb0f9760d55e4912a88d8171295d Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Tue, 22 Jul 2025 15:11:33 +0100 Subject: [PATCH 20/20] Add tests --- .../firebase/otel/patches/firestore.ts | 36 +++++---- .../integrations/tracing/firebase.test.ts | 74 +++++++++++++++++++ 2 files changed, 96 insertions(+), 14 deletions(-) create mode 100644 packages/node/test/integrations/tracing/firebase.test.ts diff --git a/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts b/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts index e8c4a99bbf71..b450be959b69 100644 --- a/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts +++ b/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts @@ -253,22 +253,25 @@ function startDBSpan( } /** - * Sets the server address and port attributes on the span based on the Firestore settings. + * Gets the server address and port attributes from the Firestore settings. * It's best effort to extract the address and port from the settings, especially for IPv6. * @param span - The span to set attributes on. * @param settings - The Firestore settings containing host information. */ -function setPortAndAddress(span: Span, settings: FirestoreSettings): void { - if (typeof settings.host === 'string') { - let address: string | undefined; - let port: string | undefined; +export function getPortAndAddress(settings: FirestoreSettings): { + address?: string; + port?: number; +} { + let address: string | undefined; + let port: string | undefined; + if (typeof settings.host === 'string') { if (settings.host.startsWith('[')) { // IPv6 addresses can be enclosed in square brackets, e.g., [2001:db8::1]:8080 if (settings.host.endsWith(']')) { // IPv6 with square brackets without port address = settings.host.replace(/^\[|\]$/g, ''); - } else { + } else if (settings.host.includes(']:')) { // IPv6 with square brackets with port const lastColonIndex = settings.host.lastIndexOf(':'); if (lastColonIndex !== -1) { @@ -293,14 +296,11 @@ function setPortAndAddress(span: Span, settings: FirestoreSettings): void { } } } - if (address) { - span.setAttribute(ATTR_SERVER_ADDRESS, address); - } - - if (port !== undefined) { - span.setAttribute(ATTR_SERVER_PORT, Number(port)); - } } + return { + address: address, + port: port ? parseInt(port, 10) : undefined, + }; } function addAttributes( @@ -323,6 +323,14 @@ function addAttributes( 'firebase.firestore.options.storageBucket': firestoreOptions.storageBucket, }; + const { address, port } = getPortAndAddress(settings); + + if (address) { + attributes[ATTR_SERVER_ADDRESS] = address; + } + if (port) { + attributes[ATTR_SERVER_PORT] = port; + } + span.setAttributes(attributes); - setPortAndAddress(span, settings); } diff --git a/packages/node/test/integrations/tracing/firebase.test.ts b/packages/node/test/integrations/tracing/firebase.test.ts new file mode 100644 index 000000000000..0fe0309f4449 --- /dev/null +++ b/packages/node/test/integrations/tracing/firebase.test.ts @@ -0,0 +1,74 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { getPortAndAddress } from '../../../src/integrations/tracing/firebase/otel/patches/firestore'; + +describe('setPortAndAddress', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('IPv6 addresses', () => { + it('should correctly parse IPv6 address without port', () => { + const { address, port } = getPortAndAddress({ host: '[2001:db8::1]' }); + + expect(address).toBe('2001:db8::1'); + expect(port).toBeUndefined(); + }); + + it('should correctly parse IPv6 address with port', () => { + const { address, port } = getPortAndAddress({ host: '[2001:db8::1]:8080' }); + expect(address).toBe('2001:db8::1'); + expect(port).toBe(8080); + }); + + it('should handle IPv6 localhost without port', () => { + const { address, port } = getPortAndAddress({ host: '[::1]' }); + + expect(address).toBe('::1'); + expect(port).toBeUndefined(); + }); + + it('should handle IPv6 localhost with port', () => { + const { address, port } = getPortAndAddress({ host: '[::1]:3000' }); + + expect(address).toBe('::1'); + expect(port).toBe(3000); + }); + }); + + describe('IPv4 and hostname addresses', () => { + it('should correctly parse IPv4 address with port', () => { + const { address, port } = getPortAndAddress({ host: '192.168.1.1:8080' }); + + expect(address).toBe('192.168.1.1'); + expect(port).toBe(8080); + }); + + it('should correctly parse hostname with port', () => { + const { address, port } = getPortAndAddress({ host: 'localhost:3000' }); + + expect(address).toBe('localhost'); + expect(port).toBe(3000); + }); + + it('should correctly parse hostname without port', () => { + const { address, port } = getPortAndAddress({ host: 'example.com' }); + + expect(address).toBe('example.com'); + expect(port).toBeUndefined(); + }); + + it('should correctly parse hostname with port', () => { + const { address, port } = getPortAndAddress({ host: 'example.com:4000' }); + + expect(address).toBe('example.com'); + expect(port).toBe(4000); + }); + + it('should handle empty string', () => { + const { address, port } = getPortAndAddress({ host: '' }); + + expect(address).toBe(''); + expect(port).toBeUndefined(); + }); + }); +});