UNPKG

@sentry/node

Version:

Sentry Node SDK using OpenTelemetry for performance instrumentation

296 lines (264 loc) 8.62 kB
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); const net = require('node:net'); const api = require('@opentelemetry/api'); const instrumentation = require('@opentelemetry/instrumentation'); const semanticConventions = require('@opentelemetry/semantic-conventions'); /** * * @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 */ function patchFirestore( tracer, firestoreSupportedVersions, wrap, unwrap, config, ) { // eslint-disable-next-line @typescript-eslint/no-empty-function const defaultFirestoreSpanCreationHook = () => {}; let firestoreSpanCreationHook = defaultFirestoreSpanCreationHook; const configFirestoreSpanCreationHook = config.firestoreSpanCreationHook; if (typeof configFirestoreSpanCreationHook === 'function') { firestoreSpanCreationHook = (span) => { instrumentation.safeExecuteInTheMiddle( () => configFirestoreSpanCreationHook(span), error => { if (!error) { return; } api.diag.error(error?.message); }, true, ); }; } const moduleFirestoreCJS = new instrumentation.InstrumentationNodeModuleDefinition( '@firebase/firestore', firestoreSupportedVersions, // eslint-disable-next-line @typescript-eslint/no-explicit-any (moduleExports) => wrapMethods(moduleExports, wrap, unwrap, tracer, firestoreSpanCreationHook), ); const files = [ '@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 instrumentation.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, wrap, unwrap, tracer, firestoreSpanCreationHook, // eslint-disable-next-line @typescript-eslint/no-explicit-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, unwrap, // eslint-disable-next-line @typescript-eslint/no-explicit-any ) { for (const method of ['addDoc', 'getDocs', 'setDoc', 'deleteDoc']) { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access if (instrumentation.isWrapped(moduleExports[method])) { // eslint-disable-next-line @typescript-eslint/no-unsafe-argument unwrap(moduleExports, method); } } return moduleExports; } function patchAddDoc( tracer, firestoreSpanCreationHook, ) { return function addDoc(original) { return function ( reference, data, ) { const span = startDBSpan(tracer, 'addDoc', reference); firestoreSpanCreationHook(span); return executeContextWithSpan(span, () => { return original(reference, data); }); }; }; } function patchDeleteDoc( tracer, firestoreSpanCreationHook, ) { return function deleteDoc(original) { return function (reference) { const span = startDBSpan(tracer, 'deleteDoc', reference.parent || reference); firestoreSpanCreationHook(span); return executeContextWithSpan(span, () => { return original(reference); }); }; }; } function patchGetDocs( tracer, firestoreSpanCreationHook, ) { return function getDocs(original) { return function ( reference, ) { const span = startDBSpan(tracer, 'getDocs', reference); firestoreSpanCreationHook(span); return executeContextWithSpan(span, () => { return original(reference); }); }; }; } function patchSetDoc( tracer, firestoreSpanCreationHook, ) { return function setDoc(original) { return function ( reference, data, options, ) { const span = startDBSpan(tracer, 'setDoc', reference.parent || reference); firestoreSpanCreationHook(span); return executeContextWithSpan(span, () => { return typeof options !== 'undefined' ? original(reference, data, options) : original(reference, data); }); }; }; } function executeContextWithSpan(span, callback) { return api.context.with(api.trace.setSpan(api.context.active(), span), () => { return instrumentation.safeExecuteInTheMiddle( () => { return callback(); }, err => { if (err) { span.recordException(err); } span.end(); }, true, ); }); } function startDBSpan( tracer, spanName, reference, ) { const span = tracer.startSpan(`${spanName} ${reference.path}`, { kind: api.SpanKind.CLIENT }); addAttributes(span, reference); span.setAttribute(semanticConventions.ATTR_DB_OPERATION_NAME, spanName); return span; } /** * 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 getPortAndAddress(settings) { let address; let port; 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 if (settings.host.includes(']:')) { // IPv6 with square brackets with port const lastColonIndex = settings.host.lastIndexOf(':'); if (lastColonIndex !== -1) { address = settings.host.slice(1, lastColonIndex).replace(/^\[|\]$/g, ''); port = settings.host.slice(lastColonIndex + 1); } } } else { // 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; } } } } return { address: address, port: port ? parseInt(port, 10) : undefined, }; } function addAttributes( span, reference, ) { const firestoreApp = reference.firestore.app; const firestoreOptions = firestoreApp.options; const json = reference.firestore.toJSON() || {}; const settings = json.settings || {}; const attributes = { [semanticConventions.ATTR_DB_COLLECTION_NAME]: reference.path, [semanticConventions.ATTR_DB_NAMESPACE]: firestoreApp.name, [semanticConventions.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, }; const { address, port } = getPortAndAddress(settings); if (address) { attributes[semanticConventions.ATTR_SERVER_ADDRESS] = address; } if (port) { attributes[semanticConventions.ATTR_SERVER_PORT] = port; } span.setAttributes(attributes); } exports.getPortAndAddress = getPortAndAddress; exports.patchFirestore = patchFirestore; //# sourceMappingURL=firestore.js.map