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 5 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.99 kB - -
@sentry/browser - with treeshaking flags 23.76 kB - -
@sentry/browser (incl. Tracing) 38.84 kB - -
@sentry/browser (incl. Tracing, Replay) 76.98 kB - -
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 70.07 kB - -
@sentry/browser (incl. Tracing, Replay with Canvas) 81.74 kB - -
@sentry/browser (incl. Tracing, Replay, Feedback) 93.83 kB - -
@sentry/browser (incl. Feedback) 40.75 kB - -
@sentry/browser (incl. sendFeedback) 28.7 kB - -
@sentry/browser (incl. FeedbackAsync) 33.59 kB - -
@sentry/react 25.76 kB - -
@sentry/react (incl. Tracing) 40.84 kB - -
@sentry/vue 28.36 kB - -
@sentry/vue (incl. Tracing) 40.7 kB - -
@sentry/svelte 24.01 kB - -
CDN Bundle 25.5 kB - -
CDN Bundle (incl. Tracing) 38.92 kB - -
CDN Bundle (incl. Tracing, Replay) 74.83 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) 80.29 kB - -
CDN Bundle - uncompressed 74.5 kB - -
CDN Bundle (incl. Tracing) - uncompressed 115.42 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 229.47 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 242.3 kB - -
@sentry/nextjs (client) 42.47 kB - -
@sentry/sveltekit (client) 39.33 kB - -
@sentry/node 155 kB +0.46% +705 B 🔺
@sentry/node - without tracing 98.64 kB +0.01% +2 B 🔺
@sentry/aws-serverless 124.4 kB -0.01% -1 B 🔽

View base workflow run

@onurtemizkan onurtemizkan force-pushed the onur/firebase-instrumentation branch 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();
}

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
2 participants
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy