Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/crashlytics/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -117,12 +117,12 @@
"@firebase/installations": "0.6.20",
"@opentelemetry/api": "1.9.0",
"@opentelemetry/api-logs": "0.212.0",
"@opentelemetry/context-zone": "2.5.1",
"@opentelemetry/core": "2.5.1",
"@opentelemetry/exporter-logs-otlp-http": "0.212.0",
"@opentelemetry/exporter-trace-otlp-proto": "0.212.0",
"@opentelemetry/instrumentation": "0.212.0",
"@opentelemetry/instrumentation-fetch": "0.212.0",
"@opentelemetry/instrumentation-user-interaction": "0.56.0",
"@opentelemetry/instrumentation-xml-http-request": "0.213.0",
"@opentelemetry/otlp-exporter-base": "0.212.0",
"@opentelemetry/otlp-transformer": "0.212.0",
Expand Down Expand Up @@ -175,4 +175,4 @@
],
"reportDir": "./coverage/node"
}
}
}
110 changes: 58 additions & 52 deletions packages/crashlytics/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { LOG_ENTRY_ATTRIBUTE_KEYS, CRASHLYTICS_TYPE } from './constants';
import { Crashlytics, CrashlyticsOptions } from './public-types';
import { Provider } from '@firebase/component';
import { AnyValueMap, SeverityNumber } from '@opentelemetry/api-logs';
import { trace } from '@opentelemetry/api';
import { trace, context } from '@opentelemetry/api';
import { CrashlyticsService } from './service';
import { flush, getAppVersion, getSessionId } from './helpers';
import { CrashlyticsInternal } from './types';
Expand Down Expand Up @@ -83,66 +83,72 @@ export function recordError(
const logger = (crashlytics as CrashlyticsInternal).loggerProvider.getLogger(
'error-logger'
);
const currentSessionSpan = (crashlytics as CrashlyticsInternal).currentSessionSpan;
const customAttributes: AnyValueMap = {};

// Add framework-specific metadata
const frameworkAttributesProvider = (crashlytics as CrashlyticsService)
.frameworkAttributesProvider;
if (frameworkAttributesProvider) {
const frameworkAttributes = frameworkAttributesProvider();
Object.assign(customAttributes, frameworkAttributes);
}
const logError = (): void => {
// Add framework-specific metadata
const frameworkAttributesProvider = (crashlytics as CrashlyticsService)
.frameworkAttributesProvider;
if (frameworkAttributesProvider) {
const frameworkAttributes = frameworkAttributesProvider();
Object.assign(customAttributes, frameworkAttributes);
}

// Add trace metadata
const activeSpanContext = trace.getActiveSpan()?.spanContext();
if (crashlytics.app.options.projectId && activeSpanContext?.traceId) {
customAttributes[
'logging.googleapis.com/trace'
] = `projects/${crashlytics.app.options.projectId}/traces/${activeSpanContext.traceId}`;
if (activeSpanContext?.spanId) {
customAttributes['logging.googleapis.com/spanId'] =
activeSpanContext.spanId;
// Add trace metadata
const activeSpanContext = trace.getActiveSpan()?.spanContext();
if (activeSpanContext?.traceId) {
customAttributes[LOG_ENTRY_ATTRIBUTE_KEYS.TRACE_ID] = activeSpanContext.traceId;
if (activeSpanContext?.spanId) {
customAttributes[LOG_ENTRY_ATTRIBUTE_KEYS.SPAN_ID] = activeSpanContext.spanId;
}
}
}

// Add app version metadata
customAttributes[LOG_ENTRY_ATTRIBUTE_KEYS.APP_VERSION] =
getAppVersion(crashlytics);
// Add app version metadata
customAttributes[LOG_ENTRY_ATTRIBUTE_KEYS.APP_VERSION] =
getAppVersion(crashlytics);

// Add session ID metadata
const sessionId = getSessionId();
if (sessionId) {
customAttributes[LOG_ENTRY_ATTRIBUTE_KEYS.SESSION_ID] = sessionId;
}
// Add session ID metadata
const sessionId = getSessionId();
if (sessionId) {
customAttributes[LOG_ENTRY_ATTRIBUTE_KEYS.SESSION_ID] = sessionId;
}

// Merge in any additional attributes. Explicitly provided attributes take precedence over
// automatically added attributes.
if (attributes) {
Object.assign(customAttributes, attributes);
}
// Merge in any additional attributes. Explicitly provided attributes take precedence over
// automatically added attributes.
if (attributes) {
Object.assign(customAttributes, attributes);
}

if (error instanceof Error) {
logger.emit({
severityNumber: SeverityNumber.ERROR,
body: error.message,
attributes: {
'error.type': error.name || 'Error',
'error.stack': error.stack || 'No stack trace available',
...customAttributes
}
});
} else if (typeof error === 'string') {
logger.emit({
severityNumber: SeverityNumber.ERROR,
body: error,
attributes: customAttributes
});
if (error instanceof Error) {
logger.emit({
severityNumber: SeverityNumber.ERROR,
body: error.message,
attributes: {
'error.type': error.name || 'Error',
'error.stack': error.stack || 'No stack trace available',
...customAttributes
}
});
} else if (typeof error === 'string') {
logger.emit({
severityNumber: SeverityNumber.ERROR,
body: error,
attributes: customAttributes
});
} else {
logger.emit({
severityNumber: SeverityNumber.ERROR,
body: `Unknown error type: ${typeof error}`,
attributes: customAttributes
});
}
};

if (!trace.getActiveSpan() && currentSessionSpan) {
context.with(trace.setSpan(context.active(), currentSessionSpan), logError);
} else {
logger.emit({
severityNumber: SeverityNumber.ERROR,
body: `Unknown error type: ${typeof error}`,
attributes: customAttributes
});
logError();
}
}

Expand Down
4 changes: 3 additions & 1 deletion packages/crashlytics/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ export const CRASHLYTICS_SESSION_ID_KEY = 'firebasecrashlytics.sessionid';
export const LOG_ENTRY_ATTRIBUTE_KEYS = {
APP_VERSION: 'app_version',
SESSION_ID: 'session_id',
USER_ID: 'user_id'
USER_ID: 'user_id',
TRACE_ID: 'logging.googleapis.com/trace',
SPAN_ID: 'logging.googleapis.com/spanId'
};

/**
Expand Down
47 changes: 40 additions & 7 deletions packages/crashlytics/src/helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ describe('helpers', () => {

const fakeTracingProvider = {
getTracer: () => ({
startSpan: () => ({
setAttribute: () => { },
end: () => { }
}),
startActiveSpan: (name: string, fn: (span: any) => any) =>
fn({
end: () => {},
Expand Down Expand Up @@ -135,6 +139,7 @@ describe('helpers', () => {
[LOG_ENTRY_ATTRIBUTE_KEYS.SESSION_ID]: MOCK_SESSION_ID,
[LOG_ENTRY_ATTRIBUTE_KEYS.APP_VERSION]: 'unset'
});
expect(fakeCrashlytics.currentSessionSpan).to.not.be.undefined;
});

it('should log app version from AUTO_CONSTANTS', () => {
Expand All @@ -145,6 +150,7 @@ describe('helpers', () => {
[LOG_ENTRY_ATTRIBUTE_KEYS.SESSION_ID]: MOCK_SESSION_ID,
[LOG_ENTRY_ATTRIBUTE_KEYS.APP_VERSION]: '1.2.3'
});
expect(fakeCrashlytics.currentSessionSpan).to.not.be.undefined;
});

it('should log app version from telemetry options', () => {
Expand All @@ -161,6 +167,7 @@ describe('helpers', () => {
[LOG_ENTRY_ATTRIBUTE_KEYS.SESSION_ID]: MOCK_SESSION_ID,
[LOG_ENTRY_ATTRIBUTE_KEYS.APP_VERSION]: '9.9.9'
});
expect(telemetryWithVersion.currentSessionSpan).to.not.be.undefined;
});
});

Expand All @@ -185,13 +192,39 @@ describe('helpers', () => {
});

it('should flush logs when the pagehide event fires', () => {
registerListeners(fakeCrashlytics);

expect(flushed).to.be.false;

window.dispatchEvent(new Event('pagehide'));

expect(flushed).to.be.true;
let spanEnded = false;
const mockSpan = {
setAttribute: () => { },
end: () => {
spanEnded = true;
}
};
const mockTracer = {
startSpan: () => mockSpan,
startActiveSpan: (name: string, fn: (span: any) => any) =>
fn({
end: () => { },
spanContext: () => ({ traceId: 'my-trace', spanId: 'my-span' })
})
};
// Override getTracer to return our mock tracer
const originalGetTracer = fakeTracingProvider.getTracer;
fakeTracingProvider.getTracer = () => mockTracer as any;

try {
startNewSession(fakeCrashlytics);
registerListeners(fakeCrashlytics);

expect(flushed).to.be.false;
expect(spanEnded).to.be.false;

window.dispatchEvent(new Event('pagehide'));

expect(flushed).to.be.true;
expect(spanEnded).to.be.true;
} finally {
fakeTracingProvider.getTracer = originalGetTracer;
}
});
}
});
Expand Down
34 changes: 20 additions & 14 deletions packages/crashlytics/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
import { Crashlytics } from './public-types';
import { CrashlyticsService } from './service';
import { CrashlyticsInternal } from './types';
import { sessionContextManager } from './tracing/tracing-provider';

/**
* Returns the app version from the provided Telemetry instance, if available.
Expand All @@ -37,18 +38,7 @@ export function getAppVersion(crashlytics: Crashlytics): string {
return 'unset';
}

/**
* Returns the session ID stored in sessionStorage, if available.
*/
export function getSessionId(): string | undefined {
if (typeof sessionStorage !== 'undefined') {
try {
return sessionStorage.getItem(CRASHLYTICS_SESSION_ID_KEY) || undefined;
} catch (e) {
// Ignore errors accessing sessionStorage (e.g. security restrictions)
}
}
}
export { getSessionId } from './session';

/**
* Generate a new session UUID. We record it in two places:
Expand All @@ -57,14 +47,26 @@ export function getSessionId(): string | undefined {
*/
export function startNewSession(crashlytics: Crashlytics): void {
// Cast to CrashlyticsInternal to access internal loggerProvider
const { loggerProvider } = crashlytics as CrashlyticsInternal;
const { loggerProvider, tracingProvider } = crashlytics as CrashlyticsInternal;

if (
typeof sessionStorage !== 'undefined' &&
typeof crypto?.randomUUID === 'function'
) {
try {
const sessionId = crypto.randomUUID();
sessionStorage.setItem(CRASHLYTICS_SESSION_ID_KEY, sessionId);
console.log('Session started with ID: ', sessionId);

const tracer = tracingProvider.getTracer('session-tracer');
const span = tracer.startSpan('session-start');
span.setAttribute(LOG_ENTRY_ATTRIBUTE_KEYS.SESSION_ID, sessionId);
span.setAttribute(
LOG_ENTRY_ATTRIBUTE_KEYS.APP_VERSION,
getAppVersion(crashlytics)
);
(crashlytics as CrashlyticsInternal).currentSessionSpan = span;
sessionContextManager.setSessionSpan(span);

// Emit session creation log
const logger = loggerProvider.getLogger('session-logger');
Expand All @@ -73,7 +75,9 @@ export function startNewSession(crashlytics: Crashlytics): void {
body: 'Session created',
attributes: {
[LOG_ENTRY_ATTRIBUTE_KEYS.SESSION_ID]: sessionId,
[LOG_ENTRY_ATTRIBUTE_KEYS.APP_VERSION]: getAppVersion(crashlytics)
[LOG_ENTRY_ATTRIBUTE_KEYS.APP_VERSION]: getAppVersion(crashlytics),
[LOG_ENTRY_ATTRIBUTE_KEYS.TRACE_ID]: `${span.spanContext().traceId}`,
[LOG_ENTRY_ATTRIBUTE_KEYS.SPAN_ID]: `${span.spanContext().spanId}`
}
});
} catch (e) {
Expand All @@ -90,10 +94,12 @@ export function registerListeners(crashlytics: Crashlytics): void {
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
window.addEventListener('visibilitychange', async () => {
if (document.visibilityState === 'hidden') {
(crashlytics as CrashlyticsInternal).currentSessionSpan?.end();
await flush(crashlytics);
}
});
window.addEventListener('pagehide', async () => {
(crashlytics as CrashlyticsInternal).currentSessionSpan?.end();
await flush(crashlytics);
});
}
Expand Down
19 changes: 13 additions & 6 deletions packages/crashlytics/src/logging/logger-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
LoggerProvider,
BatchLogRecordProcessor,
ReadableLogRecord,
LogRecordExporter
LogRecordExporter,
ConsoleLogRecordExporter,
SimpleLogRecordProcessor
} from '@opentelemetry/sdk-logs';
import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions';
import { resourceFromAttributes } from '@opentelemetry/resources';
Expand All @@ -33,6 +35,7 @@
import { DynamicHeaderProvider, DynamicLogAttributeProvider } from '../types';
import { FirebaseApp } from '@firebase/app';
import { ExportResult } from '@opentelemetry/core';
import { LOG_ENTRY_ATTRIBUTE_KEYS } from '../constants';

Check failure on line 38 in packages/crashlytics/src/logging/logger-provider.ts

View workflow job for this annotation

GitHub Actions / Lint

'LOG_ENTRY_ATTRIBUTE_KEYS' is defined but never used. Allowed unused vars must match /^_/u

Check failure on line 38 in packages/crashlytics/src/logging/logger-provider.ts

View workflow job for this annotation

GitHub Actions / Lint

'LOG_ENTRY_ATTRIBUTE_KEYS' is defined but never used

/**
* Create a logger provider for the current execution environment.
Expand Down Expand Up @@ -68,7 +71,10 @@

return new LoggerProvider({
resource,
processors: [new BatchLogRecordProcessor(logExporter)]
processors: [
new SimpleLogRecordProcessor(new ConsoleLogRecordExporter()),
new BatchLogRecordProcessor(logExporter)
]
});
}

Expand Down Expand Up @@ -115,11 +121,12 @@
attributes.filter((attr): attr is [string, string] => attr != null)
);

if (Object.keys(attributesToApply).length > 0) {
logs.forEach(log => {
logs.forEach(log => {
if (Object.keys(attributesToApply).length > 0) {
Object.assign(log.attributes, attributesToApply);
});
}
}
});

super.export(logs, resultCallback);
}

Expand Down
3 changes: 2 additions & 1 deletion packages/crashlytics/src/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@
import { _FirebaseService, FirebaseApp } from '@firebase/app';
import { Crashlytics, CrashlyticsOptions } from './public-types';
import { LoggerProvider } from '@opentelemetry/sdk-logs';
import { TracerProvider } from '@opentelemetry/api';
import { TracerProvider, Span } from '@opentelemetry/api';

export class CrashlyticsService implements Crashlytics, _FirebaseService {
private _options?: CrashlyticsOptions;
private _frameworkAttributesProvider?: () => Record<string, string>;
public currentSessionSpan?: Span;

Check failure on line 26 in packages/crashlytics/src/service.ts

View workflow job for this annotation

GitHub Actions / Lint

Public accessibility modifier on class property currentSessionSpan

constructor(
public app: FirebaseApp,
Expand Down
Loading
Loading