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 8 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.78 kB - -
@sentry/browser - with treeshaking flags 22.34 kB - -
@sentry/browser (incl. Tracing) 39.66 kB - -
@sentry/browser (incl. Tracing, Replay) 77.79 kB - -
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 67.59 kB - -
@sentry/browser (incl. Tracing, Replay with Canvas) 82.49 kB - -
@sentry/browser (incl. Tracing, Replay, Feedback) 94.59 kB - -
@sentry/browser (incl. Feedback) 40.48 kB - -
@sentry/browser (incl. sendFeedback) 28.46 kB - -
@sentry/browser (incl. FeedbackAsync) 33.36 kB - -
@sentry/react 25.54 kB - -
@sentry/react (incl. Tracing) 41.62 kB - -
@sentry/vue 28.23 kB - -
@sentry/vue (incl. Tracing) 41.45 kB - -
@sentry/svelte 23.81 kB - -
CDN Bundle 25.18 kB - -
CDN Bundle (incl. Tracing) 39.42 kB - -
CDN Bundle (incl. Tracing, Replay) 75.42 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) 80.89 kB - -
CDN Bundle - uncompressed 73.44 kB - -
CDN Bundle (incl. Tracing) - uncompressed 116.86 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 231 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 243.81 kB - -
@sentry/nextjs (client) 43.7 kB - -
@sentry/sveltekit (client) 40.08 kB - -
@sentry/node 168.56 kB +0.45% +742 B 🔺
@sentry/node - without tracing 100.2 kB - -
@sentry/aws-serverless 128.36 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 +253 to +258
spanName: string,
reference: CollectionReference<AppModelType, DbModelType> | DocumentReference<AppModelType, DbModelType>,
): Span {
const span = tracer.startSpan(`${spanName} ${reference.path}`, { kind: SpanKind.CLIENT });
addAttributes(span, reference);
span.setAttribute(ATTR_DB_OPERATION_NAME, spanName);

Choose a reason for hiding this comment

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

The function addAttributes could benefit from defensive coding to handle cases where reference.firestore.toJSON() returns undefined or throws an error.

Suggested change
spanName: string,
reference: CollectionReference<AppModelType, DbModelType> | DocumentReference<AppModelType, DbModelType>,
): Span {
const span = tracer.startSpan(`${spanName} ${reference.path}`, { kind: SpanKind.CLIENT });
addAttributes(span, reference);
span.setAttribute(ATTR_DB_OPERATION_NAME, spanName);
function addAttributes<AppModelType, DbModelType extends DocumentData>(
span: Span,
reference: CollectionReference<AppModelType, DbModelType> | DocumentReference<AppModelType, DbModelType>,
): void {
try {
const firestoreApp: FirebaseApp = reference.firestore.app;
const firestoreOptions: FirebaseOptions = firestoreApp.options;
const json: { settings?: FirestoreSettings } = reference.firestore.toJSON() || {};
const settings: FirestoreSettings = json.settings || {};

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 +23 to +25

// Inlined types from 'firebase/firestore'
export interface DocumentData {

Choose a reason for hiding this comment

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

The DocumentData interface allows any field which could lead to type safety issues. Consider adding stricter typing or validation.

Suggested change
// Inlined types from 'firebase/firestore'
export interface DocumentData {
export interface DocumentData {
readonly [field: string]: unknown; // Use unknown instead of any for better type safety
}

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

Comment on lines +230 to +240
};
};
}

function executeContextWithSpan<T>(span: Span, callback: () => T): T {
return context.with(trace.setSpan(context.active(), span), () => {
return safeExecuteInTheMiddle(
(): T => {
return callback();
},
err => {

Choose a reason for hiding this comment

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

The span.recordException call should be wrapped in a try-catch to prevent instrumentation from breaking the application if exception recording fails.

Suggested change
};
};
}
function executeContextWithSpan<T>(span: Span, callback: () => T): T {
return context.with(trace.setSpan(context.active(), span), () => {
return safeExecuteInTheMiddle(
(): T => {
return callback();
},
err => {
err => {
if (err) {
try {
span.recordException(err);
} catch (recordError) {
// Log but don't throw to prevent breaking the application
diag.error('Failed to record exception on span:', recordError);
}
}
span.end();
}

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.

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?

});
}

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.

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Bug: Firebase API Function Naming Inconsistency

The span name and db.operation.name attribute for the setDoc operation are incorrectly set to 'setDocs'. This should be 'setDoc' to match the Firebase API function and maintain consistency with other instrumented operations like addDoc, getDocs, and deleteDoc.

packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts#L222-L223

): Promise<void> {
const span = startDBSpan(tracer, 'setDocs', reference.parent || reference);

Fix in CursorFix in Web


Bug: Missing Method Unwrapping Causes Cleanup Issues

The unwrapMethods function is missing the unwrapping of the deleteDoc method. While deleteDoc is wrapped by wrapMethods, unwrapMethods only unwraps addDoc, getDocs, and setDoc. This inconsistency can lead to memory leaks or incorrect behavior during module cleanup or reloading.

packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts#L118-L141

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

Fix in CursorFix in Web


Was this report helpful? Give feedback by reacting with 👍 or 👎

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