Skip to content

feat(node): Add firebase integration #16719

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 26 commits into
base: develop
Choose a base branch
from

Conversation

onurtemizkan
Copy link
Collaborator

@onurtemizkan onurtemizkan commented Jun 24, 2025

Continued work on #13954
Resolves: #13678

Adds instrumentation for Firebase / Firestore queries.

Updates on top of #13954:

Copy link
Contributor

github-actions bot commented Jun 24, 2025

size-limit report 📦

Path Size % Change Change
@sentry/browser 23.76 kB - -
@sentry/browser - with treeshaking flags 22.35 kB - -
@sentry/browser (incl. Tracing) 39.41 kB - -
@sentry/browser (incl. Tracing, Replay) 77.52 kB - -
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 67.39 kB - -
@sentry/browser (incl. Tracing, Replay with Canvas) 82.23 kB - -
@sentry/browser (incl. Tracing, Replay, Feedback) 94.35 kB - -
@sentry/browser (incl. Feedback) 40.45 kB - -
@sentry/browser (incl. sendFeedback) 28.44 kB - -
@sentry/browser (incl. FeedbackAsync) 33.34 kB - -
@sentry/react 25.5 kB - -
@sentry/react (incl. Tracing) 41.38 kB - -
@sentry/vue 28.2 kB - -
@sentry/vue (incl. Tracing) 41.21 kB - -
@sentry/svelte 23.79 kB - -
CDN Bundle 25.28 kB - -
CDN Bundle (incl. Tracing) 39.28 kB - -
CDN Bundle (incl. Tracing, Replay) 75.39 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) 80.82 kB - -
CDN Bundle - uncompressed 73.86 kB - -
CDN Bundle (incl. Tracing) - uncompressed 116.29 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 230.53 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 243.35 kB - -
@sentry/nextjs (client) 43.44 kB - -
@sentry/sveltekit (client) 39.84 kB - -
@sentry/node-core 47.48 kB - -
@sentry/node 145.02 kB +0.62% +892 B 🔺
@sentry/node - without tracing 91.58 kB - -
@sentry/aws-serverless 103.03 kB - -

View base workflow run

@onurtemizkan onurtemizkan force-pushed the onur/firebase-instrumentation branch 2 times, most recently from 84c5d6a to ea8c176 Compare June 26, 2025 12:14
@onurtemizkan onurtemizkan marked this pull request as ready for review June 26, 2025 12:31
@AbhiPrasad
Copy link
Member

@sentry review

Copy link

On it! We are reviewing the PR and will provide feedback shortly.

Copy link

PR Description

This pull request introduces a new Firebase integration for Sentry Node, enabling automatic instrumentation of Firebase Firestore operations. The goal is to provide out-of-the-box performance monitoring and tracing for applications using Firebase, allowing developers to quickly identify and resolve performance bottlenecks within their Firebase interactions.

Click to see more

Key Technical Changes

The key technical changes include:

  • Firebase Integration: A new firebaseIntegration is added to @sentry/node, which automatically instruments Firebase Firestore.
  • OpenTelemetry Instrumentation: The integration leverages OpenTelemetry (OTEL) to create spans for Firestore operations like addDoc, getDocs, setDoc, and deleteDoc. This follows the existing pattern of other tracing integrations.
  • Shimmer Patching: The integration uses shimmer to patch the @firebase/firestore module, wrapping the relevant functions to create spans before and after their execution.
  • Span Attributes: Span attributes are added to provide context about the Firestore operation, including collection name, database namespace, project ID, server address, and port.
  • E2E Tests: An end-to-end test application (node-firebase) is created to verify the integration's functionality. This includes a Docker setup for running Firebase emulators and Playwright tests to assert that spans are correctly generated.
  • Configuration: The integration provides a firestoreSpanCreationHook to allow users to customize the created spans.
  • Docker Setup: Docker compose and associated scripts are added to facilitate local testing with the Firebase emulator.

Architecture Decisions

The architectural decisions include:

  • OTEL-based Instrumentation: Using OpenTelemetry for instrumentation ensures consistency with other Sentry Node tracing integrations and allows for future expansion to other Firebase services.
  • Span Creation Hook: Providing a firestoreSpanCreationHook allows users to customize span attributes or add additional context, offering flexibility without requiring changes to the core integration.
  • Lite Firestore Library: The e2e test application uses firebase/firestore/lite to avoid gRPC dependencies, simplifying the Docker setup.

Dependencies and Interactions

This integration depends on the following:

  • @sentry/node: The core Sentry Node SDK.
  • @opentelemetry/api: The OpenTelemetry API for creating and managing spans.
  • @opentelemetry/instrumentation: The OpenTelemetry instrumentation library for patching modules.
  • shimmer: Used for function wrapping.
  • firebase/app and @firebase/firestore: The Firebase SDK.

The integration interacts with the Firebase Firestore service by intercepting calls to its API. It also interacts with the Sentry backend by sending transaction events containing the generated spans.

Risk Considerations

The potential risks and considerations include:

  • Performance Overhead: The instrumentation could introduce some performance overhead, although OTEL is designed to minimize this. Thorough testing is needed to ensure the impact is acceptable.
  • Compatibility: The integration is designed for specific versions of @firebase/firestore. Compatibility with future versions needs to be maintained.
  • Security: Ensure no sensitive data is inadvertently captured in span attributes. Consider providing options to filter or redact sensitive information.
  • Error Handling: Robust error handling is crucial to prevent the instrumentation from breaking the application if something goes wrong during span creation or attribute setting.

Notable Implementation Details

Notable implementation details include:

  • The use of generateInstrumentOnce to ensure the Firebase instrumentation is only applied once.
  • The safeExecuteInTheMiddle function is used to execute the original Firebase functions and the span ending logic, ensuring that errors in the instrumentation don't prevent the Firebase operations from completing.
  • The Docker setup includes scripts to create and manage environment variables and Firebase configuration, simplifying the testing process.

Comment on lines +66 to +67
}
diag.error(error?.message);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The diag.error(error?.message) may not provide sufficient context. Consider logging the full error object or providing more descriptive error messages with operation context.

Suggested change
}
diag.error(error?.message);
diag.error('Firebase Firestore span creation hook failed:', error);

Comment on lines 259 to 269
return span;
}

function addAttributes<AppModelType, DbModelType extends DocumentData>(
span: Span,
reference: CollectionReference<AppModelType, DbModelType> | DocumentReference<AppModelType, DbModelType>,
): void {
const firestoreApp: FirebaseApp = reference.firestore.app;
const firestoreOptions: FirebaseOptions = firestoreApp.options;
const json: { settings?: FirestoreSettings } = reference.firestore.toJSON() || {};
const settings: FirestoreSettings = json.settings || {};

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider sanitizing sensitive data from Firebase options before adding them as span attributes. ProjectId and appId may be acceptable, but other fields might contain sensitive information.

Suggested change
return span;
}
function addAttributes<AppModelType, DbModelType extends DocumentData>(
span: Span,
reference: CollectionReference<AppModelType, DbModelType> | DocumentReference<AppModelType, DbModelType>,
): 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,
// Consider if these should be included for security reasons
// 'firebase.firestore.options.appId': firestoreOptions.appId,
// 'firebase.firestore.options.messagingSenderId': firestoreOptions.messagingSenderId,
// 'firebase.firestore.options.storageBucket': firestoreOptions.storageBucket,
};

Comment on lines 67 to 72
if (typeof filePathUpdateNotifierFirebaseTools !== 'string') {
throw new Error('no CONFIG_UPDATE_NOTIFIER_FIREBASE_TOOLS environment');
}

try {
filePathFirebaseTools = JSON.parse(filePathFirebaseTools);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JSON.parse calls should be wrapped in try-catch blocks to handle malformed environment variables gracefully.

Suggested change
if (typeof filePathUpdateNotifierFirebaseTools !== 'string') {
throw new Error('no CONFIG_UPDATE_NOTIFIER_FIREBASE_TOOLS environment');
}
try {
filePathFirebaseTools = JSON.parse(filePathFirebaseTools);
try {
filePathFirebaseTools = JSON.parse(filePathFirebaseTools);
filePathUpdateNotifierFirebaseTools = JSON.parse(filePathUpdateNotifierFirebaseTools);
} catch (parseError) {
throw new Error(`Failed to parse Firebase configuration: ${parseError.message}`);
}

Comment on lines 7 to 22
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();
}
});
});

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The file creation operations lack proper error handling and could benefit from atomic writes to prevent partial file corruption.

Suggested change
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 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');
const tempPath = filePath + '.tmp';
fs.mkdirSync(filePath.substring(0, filePath.lastIndexOf('/')), { recursive: true });
fs.writeFile(tempPath, content, function (err) {
if (err) {
reject(err);
} else {
fs.rename(tempPath, filePath, (renameErr) => {
if (renameErr) {
reject(renameErr);
} else {
resolve();
}
});
}
});
});
}

Comment on lines 43 to 59

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cleanup function could be more robust by adding timeouts for graceful shutdown and fallback to SIGKILL if processes don't respond.

Suggested change
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
cleanup() {
echo "Stopping services..."
# Gracefully stop background processes with timeout
echo "Terminating background services..."
if [[ -n "$firebase_pid" ]]; then
kill -SIGTERM "$firebase_pid" || echo "Failed to terminate Firebase process"
# Wait with timeout
(sleep 10; kill -SIGKILL "$firebase_pid" 2>/dev/null) &
wait "$firebase_pid" 2>/dev/null
fi

Copy link
Member

@s1gr1d s1gr1d left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added some comments :)

import type { FirebaseInstrumentationConfig } from './types';

const DefaultFirebaseInstrumentationConfig: FirebaseInstrumentationConfig = {};
const firestoreSupportedVersions = ['>=3.0.0 <5']; // firebase 9+
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

* @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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @param unwrap - reference to native instrumentation wrap function
* @param unwrap - reference to native instrumentation unwrap function

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a minor comment but I just saw it was marked as "resolved" but the code was not changed here. Maybe wrap is correct anyway? :D

reference: CollectionReference<AppModelType, DbModelType>,
data: WithFieldValue<AppModelType>,
) => Promise<DocumentReference<AppModelType, DbModelType>> {
return function addDoc(original: AddDocType<AppModelType, DbModelType>) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it's possible to patch with a JS Proxy here (and in the other patch functions).

And I don't really understand why this needs to be recursive 🤔 It would just be good to know. Can you explain this?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's not really recursive; it's a pattern used on native OTEL instrumentations. This was part of the original implementation, and I retained it. (#13954).

I renamed the functions there to make them easier to read: f3c5ca3

});
}

function startSpan<AppModelType, DbModelType extends DocumentData>(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As Sentry exports startSpan as well, it's maybe good to rename this function to something else to see right away that this is not a Sentry function that is called. Maybe something like startDBSpan?

@onurtemizkan onurtemizkan force-pushed the onur/firebase-instrumentation branch from 1fd4ed0 to 9dfe783 Compare July 16, 2025 14:55
cursor[bot]

This comment was marked as outdated.

cursor[bot]

This comment was marked as outdated.

cursor[bot]

This comment was marked as outdated.

cursor[bot]

This comment was marked as outdated.

@onurtemizkan onurtemizkan force-pushed the onur/firebase-instrumentation branch from b308230 to 57bdae8 Compare July 18, 2025 21:57
@onurtemizkan onurtemizkan force-pushed the onur/firebase-instrumentation branch from e077257 to f3c5ca3 Compare July 21, 2025 08:29
cursor[bot]

This comment was marked as outdated.

cursor[bot]

This comment was marked as outdated.

cursor[bot]

This comment was marked as outdated.

cursor[bot]

This comment was marked as outdated.

cursor[bot]

This comment was marked as outdated.

cursor[bot]

This comment was marked as outdated.

cursor[bot]

This comment was marked as outdated.

cursor[bot]

This comment was marked as outdated.

Copy link
Member

@AbhiPrasad AbhiPrasad left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm!

@@ -0,0 +1 @@
The structure inside OTEL is to be kept as close as possible to opentelemetry plugin.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd remove this README and add this as a code comment to otel/index.ts

true,
);
});
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Firebase Spans Not Properly Managed

The executeContextWithSpan function, used for asynchronous Firebase operations, incorrectly manages span lifecycle. It uses safeExecuteInTheMiddle, which only ends the span on synchronous errors. This prevents spans from being ended when the wrapped Promise-returning function executes without immediate synchronous errors, leading to memory leaks. Additionally, spans are ended when the Promise is created, not when the async operation completes, resulting in inaccurate timing and missed error reporting for asynchronous failures.

Locations (1)

Fix in CursorFix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add firebase integration
3 participants