Skip to content

Commit 3d4fb6d

Browse files
authored
Merge branch 'main' into main
2 parents 8b0ee06 + 833f4ce commit 3d4fb6d

File tree

13 files changed

+468
-26
lines changed

13 files changed

+468
-26
lines changed

packages/telemetry/browser-telemetry/__tests__/BrowserTelemetryImpl.test.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,8 @@ it('limits pending events to maxPendingEvents', () => {
6767

6868
telemetry.register(mockClient);
6969

70-
// Should only see the last 2 errors tracked
71-
expect(mockClient.track).toHaveBeenCalledTimes(2);
70+
// Should only see the the session init event and last 2 errors tracked
71+
expect(mockClient.track).toHaveBeenCalledTimes(3);
7272
expect(mockClient.track).toHaveBeenCalledWith(
7373
'$ld:telemetry:error',
7474
expect.objectContaining({
@@ -522,3 +522,15 @@ it('uses the client logger when no logger is provided', () => {
522522
'LaunchDarkly - Browser Telemetry: Error applying breadcrumb filters: Error: Filter error',
523523
);
524524
});
525+
526+
it('sends session init event when client is registered', () => {
527+
const telemetry = new BrowserTelemetryImpl(defaultOptions);
528+
telemetry.register(mockClient);
529+
530+
expect(mockClient.track).toHaveBeenCalledWith(
531+
'$ld:telemetry:session:init',
532+
expect.objectContaining({
533+
sessionId: expect.any(String),
534+
}),
535+
);
536+
});

packages/telemetry/browser-telemetry/__tests__/options.test.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Breadcrumb } from '../src/api/Breadcrumb';
12
import ErrorCollector from '../src/collectors/error';
23
import parse, { defaultOptions } from '../src/options';
34

@@ -15,14 +16,15 @@ it('handles an empty configuration', () => {
1516
});
1617

1718
it('can set all options at once', () => {
19+
const filter = (breadcrumb: Breadcrumb) => breadcrumb;
1820
const outOptions = parse({
1921
maxPendingEvents: 1,
2022
breadcrumbs: {
2123
maxBreadcrumbs: 1,
2224
click: false,
2325
evaluations: false,
2426
flagChange: false,
25-
filters: [(breadcrumb) => breadcrumb],
27+
filters: [filter],
2628
},
2729
collectors: [new ErrorCollector(), new ErrorCollector()],
2830
});
@@ -39,7 +41,7 @@ it('can set all options at once', () => {
3941
instrumentFetch: true,
4042
instrumentXhr: true,
4143
},
42-
filters: expect.any(Array),
44+
filters: expect.arrayContaining([filter]),
4345
},
4446
stack: {
4547
source: {
@@ -50,6 +52,7 @@ it('can set all options at once', () => {
5052
},
5153
collectors: [new ErrorCollector(), new ErrorCollector()],
5254
});
55+
expect(mockLogger.warn).not.toHaveBeenCalled();
5356
});
5457

5558
it('warns when maxPendingEvents is not a number', () => {
@@ -435,6 +438,6 @@ it('warns when filters is not an array', () => {
435438
);
436439
expect(outOptions.breadcrumbs.filters).toEqual([]);
437440
expect(mockLogger.warn).toHaveBeenCalledWith(
438-
'LaunchDarkly - Browser Telemetry: Config option "breadcrumbs.filters" should be of type array, got string, using default value',
441+
'LaunchDarkly - Browser Telemetry: Config option "breadcrumbs.filters" should be of type BreadcrumbFilter[], got string, using default value',
439442
);
440443
});
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { fallbackLogger } from '../../src/logging';
2+
import { getTelemetryInstance, initTelemetry, resetTelemetryInstance } from '../../src/singleton';
3+
4+
beforeEach(() => {
5+
resetTelemetryInstance();
6+
jest.resetAllMocks();
7+
});
8+
9+
it('warns and keeps existing instance when initialized multiple times', () => {
10+
const mockLogger = {
11+
error: jest.fn(),
12+
warn: jest.fn(),
13+
info: jest.fn(),
14+
debug: jest.fn(),
15+
};
16+
17+
initTelemetry({ logger: mockLogger });
18+
const instanceA = getTelemetryInstance();
19+
initTelemetry({ logger: mockLogger });
20+
const instanceB = getTelemetryInstance();
21+
22+
expect(mockLogger.warn).toHaveBeenCalledWith(
23+
expect.stringMatching(/Telemetry has already been initialized/),
24+
);
25+
26+
expect(instanceA).toBe(instanceB);
27+
});
28+
29+
it('warns when getting telemetry instance before initialization', () => {
30+
const spy = jest.spyOn(fallbackLogger, 'warn');
31+
32+
getTelemetryInstance();
33+
34+
expect(spy).toHaveBeenCalledWith(expect.stringMatching(/Telemetry has not been initialized/));
35+
});
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import { Breadcrumb, LDClientTracking } from '../../src/api';
2+
import { BrowserTelemetry } from '../../src/api/BrowserTelemetry';
3+
import { BrowserTelemetryInspector } from '../../src/api/client/BrowserTelemetryInspector';
4+
import { getTelemetryInstance } from '../../src/singleton/singletonInstance';
5+
import {
6+
addBreadcrumb,
7+
captureError,
8+
captureErrorEvent,
9+
close,
10+
inspectors,
11+
register,
12+
} from '../../src/singleton/singletonMethods';
13+
14+
jest.mock('../../src/singleton/singletonInstance');
15+
16+
const mockTelemetry: jest.Mocked<BrowserTelemetry> = {
17+
inspectors: jest.fn(),
18+
captureError: jest.fn(),
19+
captureErrorEvent: jest.fn(),
20+
addBreadcrumb: jest.fn(),
21+
register: jest.fn(),
22+
close: jest.fn(),
23+
};
24+
25+
const mockGetTelemetryInstance = getTelemetryInstance as jest.Mock;
26+
27+
beforeEach(() => {
28+
jest.resetAllMocks();
29+
});
30+
31+
it('returns empty array when telemetry is not initialized for inspectors', () => {
32+
mockGetTelemetryInstance.mockReturnValue(undefined);
33+
expect(() => inspectors()).not.toThrow();
34+
expect(inspectors()).toEqual([]);
35+
});
36+
37+
it('returns inspectors when telemetry is initialized', () => {
38+
const mockInspectors: BrowserTelemetryInspector[] = [
39+
{ name: 'test-inspector', type: 'flag-used', synchronous: true, method: () => {} },
40+
];
41+
mockGetTelemetryInstance.mockReturnValue(mockTelemetry);
42+
mockTelemetry.inspectors.mockReturnValue(mockInspectors);
43+
44+
expect(inspectors()).toBe(mockInspectors);
45+
});
46+
47+
it('does not crash when calling captureError with no telemetry instance', () => {
48+
mockGetTelemetryInstance.mockReturnValue(undefined);
49+
const error = new Error('test error');
50+
51+
expect(() => captureError(error)).not.toThrow();
52+
53+
expect(mockTelemetry.captureError).not.toHaveBeenCalled();
54+
});
55+
56+
it('captures errors when telemetry is initialized', () => {
57+
mockGetTelemetryInstance.mockReturnValue(mockTelemetry);
58+
const error = new Error('test error');
59+
60+
captureError(error);
61+
62+
expect(mockTelemetry.captureError).toHaveBeenCalledWith(error);
63+
});
64+
65+
it('it does not crash when calling captureErrorEvent with no telemetry instance', () => {
66+
mockGetTelemetryInstance.mockReturnValue(undefined);
67+
const errorEvent = new ErrorEvent('error', { error: new Error('test error') });
68+
69+
expect(() => captureErrorEvent(errorEvent)).not.toThrow();
70+
71+
expect(mockTelemetry.captureErrorEvent).not.toHaveBeenCalled();
72+
});
73+
74+
it('captures error event when telemetry is initialized', () => {
75+
mockGetTelemetryInstance.mockReturnValue(mockTelemetry);
76+
const errorEvent = new ErrorEvent('error', { error: new Error('test error') });
77+
78+
captureErrorEvent(errorEvent);
79+
80+
expect(mockTelemetry.captureErrorEvent).toHaveBeenCalledWith(errorEvent);
81+
});
82+
83+
it('does not crash when calling addBreadcrumb with no telemetry instance', () => {
84+
mockGetTelemetryInstance.mockReturnValue(undefined);
85+
const breadcrumb: Breadcrumb = {
86+
type: 'custom',
87+
data: { test: 'data' },
88+
timestamp: Date.now(),
89+
class: 'custom',
90+
level: 'info',
91+
};
92+
93+
expect(() => addBreadcrumb(breadcrumb)).not.toThrow();
94+
95+
expect(mockTelemetry.addBreadcrumb).not.toHaveBeenCalled();
96+
});
97+
98+
it('adds breadcrumb when telemetry is initialized', () => {
99+
mockGetTelemetryInstance.mockReturnValue(mockTelemetry);
100+
const breadcrumb: Breadcrumb = {
101+
type: 'custom',
102+
data: { test: 'data' },
103+
timestamp: Date.now(),
104+
class: 'custom',
105+
level: 'info',
106+
};
107+
108+
addBreadcrumb(breadcrumb);
109+
110+
expect(mockTelemetry.addBreadcrumb).toHaveBeenCalledWith(breadcrumb);
111+
});
112+
113+
it('does not crash when calling register with no telemetry instance', () => {
114+
mockGetTelemetryInstance.mockReturnValue(undefined);
115+
const mockClient: jest.Mocked<LDClientTracking> = {
116+
track: jest.fn(),
117+
};
118+
119+
expect(() => register(mockClient)).not.toThrow();
120+
121+
expect(mockTelemetry.register).not.toHaveBeenCalled();
122+
});
123+
124+
it('registers client when telemetry is initialized', () => {
125+
mockGetTelemetryInstance.mockReturnValue(mockTelemetry);
126+
const mockClient: jest.Mocked<LDClientTracking> = {
127+
track: jest.fn(),
128+
};
129+
130+
register(mockClient);
131+
132+
expect(mockTelemetry.register).toHaveBeenCalledWith(mockClient);
133+
});
134+
135+
it('does not crash when calling close with no telemetry instance', () => {
136+
mockGetTelemetryInstance.mockReturnValue(undefined);
137+
138+
expect(() => close()).not.toThrow();
139+
140+
expect(mockTelemetry.close).not.toHaveBeenCalled();
141+
});
142+
143+
it('closes when telemetry is initialized', () => {
144+
mockGetTelemetryInstance.mockReturnValue(mockTelemetry);
145+
146+
close();
147+
148+
expect(mockTelemetry.close).toHaveBeenCalled();
149+
});

packages/telemetry/browser-telemetry/src/BrowserTelemetryImpl.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
* This is only a type dependency and these types should be compatible between
44
* SDKs.
55
*/
6-
import type { LDContext, LDEvaluationDetail, LDInspection } from '@launchdarkly/js-client-sdk';
6+
import type { LDContext, LDEvaluationDetail } from '@launchdarkly/js-client-sdk';
77

88
import { BreadcrumbFilter, LDClientLogging, LDClientTracking, MinLogger } from './api';
99
import { Breadcrumb, FeatureManagementBreadcrumb } from './api/Breadcrumb';
1010
import { BrowserTelemetry } from './api/BrowserTelemetry';
11+
import { BrowserTelemetryInspector } from './api/client/BrowserTelemetryInspector';
1112
import { Collector } from './api/Collector';
1213
import { ErrorData } from './api/ErrorData';
1314
import { EventData } from './api/EventData';
@@ -28,7 +29,7 @@ import { getTraceKit } from './vendor/TraceKit';
2829

2930
const CUSTOM_KEY_PREFIX = '$ld:telemetry';
3031
const ERROR_KEY = `${CUSTOM_KEY_PREFIX}:error`;
31-
const SESSION_CAPTURE_KEY = `${CUSTOM_KEY_PREFIX}:sessionCapture`;
32+
const SESSION_INIT_KEY = `${CUSTOM_KEY_PREFIX}:session:init`;
3233
const GENERIC_EXCEPTION = 'generic';
3334
const NULL_EXCEPTION_MESSAGE = 'exception was null or undefined';
3435
const MISSING_MESSAGE = 'exception had no message';
@@ -94,7 +95,7 @@ export default class BrowserTelemetryImpl implements BrowserTelemetry {
9495

9596
private _breadcrumbs: Breadcrumb[] = [];
9697

97-
private _inspectorInstances: LDInspection[] = [];
98+
private _inspectorInstances: BrowserTelemetryInspector[] = [];
9899
private _collectors: Collector[] = [];
99100
private _sessionId: string = randomUuidV4();
100101

@@ -149,7 +150,7 @@ export default class BrowserTelemetryImpl implements BrowserTelemetry {
149150
);
150151

151152
const impl = this;
152-
const inspectors: LDInspection[] = [];
153+
const inspectors: BrowserTelemetryInspector[] = [];
153154
makeInspectors(_options, inspectors, impl);
154155
this._inspectorInstances.push(...inspectors);
155156

@@ -163,6 +164,9 @@ export default class BrowserTelemetryImpl implements BrowserTelemetry {
163164
// When the client is registered, we need to set the logger again, because we may be able to use the client's
164165
// logger.
165166
this._setLogger();
167+
168+
this._client.track(SESSION_INIT_KEY, { sessionId: this._sessionId });
169+
166170
this._pendingEvents.forEach((event) => {
167171
this._client?.track(event.type, event.data);
168172
});
@@ -181,7 +185,7 @@ export default class BrowserTelemetryImpl implements BrowserTelemetry {
181185
}
182186
}
183187

184-
inspectors(): LDInspection[] {
188+
inspectors(): BrowserTelemetryInspector[] {
185189
return this._inspectorInstances;
186190
}
187191

@@ -237,10 +241,6 @@ export default class BrowserTelemetryImpl implements BrowserTelemetry {
237241
this.captureError(errorEvent.error);
238242
}
239243

240-
captureSession(sessionEvent: EventData): void {
241-
this._capture(SESSION_CAPTURE_KEY, { ...sessionEvent, breadcrumbs: [...this._breadcrumbs] });
242-
}
243-
244244
private _applyBreadcrumbFilters(
245245
breadcrumb: Breadcrumb,
246246
filters: BreadcrumbFilter[],

packages/telemetry/browser-telemetry/src/api/BrowserTelemetry.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import type { LDInspection } from '@launchdarkly/js-client-sdk';
2-
31
import { Breadcrumb } from './Breadcrumb';
2+
import { BrowserTelemetryInspector } from './client/BrowserTelemetryInspector';
43
import { LDClientTracking } from './client/LDClientTracking';
54

65
/**
@@ -15,9 +14,9 @@ export interface BrowserTelemetry {
1514
* Returns an array of active SDK inspectors to use with SDK versions that do
1615
* not support hooks.
1716
*
18-
* @returns An array of {@link LDInspection} objects.
17+
* @returns An array of {@link BrowserTelemetryInspector} objects.
1918
*/
20-
inspectors(): LDInspection[];
19+
inspectors(): BrowserTelemetryInspector[];
2120

2221
/**
2322
* Captures an Error object for telemetry purposes.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/**
2+
* A less constrained version of the LDInspection interface in order to allow for greater compatibility between
3+
* SDK versions.
4+
*
5+
* This interface is not intended for use by application developers and is instead intended as a compatibility bridge
6+
* to support multiple SDK versions.
7+
*/
8+
export interface BrowserTelemetryInspector {
9+
/**
10+
* The telemetry package only requires flag-detail-changed inspectors and flag-used inspectors.
11+
*/
12+
type: 'flag-used' | 'flag-detail-changed';
13+
14+
/**
15+
* The name of the inspector, used for debugging purposes.
16+
*/
17+
name: string;
18+
/**
19+
* Whether the inspector is synchronous.
20+
*/
21+
synchronous: boolean;
22+
/**
23+
* The method to call when the inspector is triggered.
24+
*
25+
* The typing here is intentionally loose to allow for greater compatibility between SDK versions.
26+
* This function should ONLY be called by an SDK instance and not by an application developer.
27+
*/
28+
method: (...args: any[]) => void;
29+
}

0 commit comments

Comments
 (0)