Skip to content

Commit db74a99

Browse files
feat: Implement browser telemetry client. (#691)
Co-authored-by: Matthew M. Keeler <[email protected]>
1 parent 70584a1 commit db74a99

File tree

9 files changed

+583
-3
lines changed

9 files changed

+583
-3
lines changed
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
import { LDClientTracking } from '../src/api/client/LDClientTracking';
2+
import BrowserTelemetryImpl from '../src/BrowserTelemetryImpl';
3+
import { ParsedOptions } from '../src/options';
4+
5+
const mockClient: jest.Mocked<LDClientTracking> = {
6+
track: jest.fn(),
7+
};
8+
9+
afterEach(() => {
10+
jest.resetAllMocks();
11+
});
12+
13+
const defaultOptions: ParsedOptions = {
14+
maxPendingEvents: 100,
15+
breadcrumbs: {
16+
maxBreadcrumbs: 50,
17+
click: true,
18+
keyboardInput: true,
19+
http: {
20+
instrumentFetch: true,
21+
instrumentXhr: true,
22+
},
23+
evaluations: true,
24+
flagChange: true,
25+
},
26+
stack: {
27+
source: {
28+
beforeLines: 5,
29+
afterLines: 5,
30+
maxLineLength: 120,
31+
},
32+
},
33+
collectors: [],
34+
};
35+
36+
it('sends buffered events when client is registered', () => {
37+
const telemetry = new BrowserTelemetryImpl(defaultOptions);
38+
const error = new Error('Test error');
39+
40+
telemetry.captureError(error);
41+
telemetry.register(mockClient);
42+
43+
expect(mockClient.track).toHaveBeenCalledWith(
44+
'$ld:telemetry:error',
45+
expect.objectContaining({
46+
type: 'Error',
47+
message: 'Test error',
48+
stack: { frames: expect.any(Array) },
49+
breadcrumbs: [],
50+
sessionId: expect.any(String),
51+
}),
52+
);
53+
});
54+
55+
it('limits pending events to maxPendingEvents', () => {
56+
const options: ParsedOptions = {
57+
...defaultOptions,
58+
maxPendingEvents: 2,
59+
};
60+
const telemetry = new BrowserTelemetryImpl(options);
61+
62+
telemetry.captureError(new Error('Error 1'));
63+
telemetry.captureError(new Error('Error 2'));
64+
telemetry.captureError(new Error('Error 3'));
65+
66+
telemetry.register(mockClient);
67+
68+
// Should only see the last 2 errors tracked
69+
expect(mockClient.track).toHaveBeenCalledTimes(2);
70+
expect(mockClient.track).toHaveBeenCalledWith(
71+
'$ld:telemetry:error',
72+
expect.objectContaining({
73+
message: 'Error 2',
74+
}),
75+
);
76+
expect(mockClient.track).toHaveBeenCalledWith(
77+
'$ld:telemetry:error',
78+
expect.objectContaining({
79+
message: 'Error 3',
80+
}),
81+
);
82+
});
83+
84+
it('manages breadcrumbs with size limit', () => {
85+
const options: ParsedOptions = {
86+
...defaultOptions,
87+
breadcrumbs: { ...defaultOptions.breadcrumbs, maxBreadcrumbs: 2 },
88+
};
89+
const telemetry = new BrowserTelemetryImpl(options);
90+
91+
telemetry.addBreadcrumb({
92+
type: 'custom',
93+
data: { id: 1 },
94+
timestamp: Date.now(),
95+
class: 'custom',
96+
level: 'info',
97+
});
98+
99+
telemetry.addBreadcrumb({
100+
type: 'custom',
101+
data: { id: 2 },
102+
timestamp: Date.now(),
103+
class: 'custom',
104+
level: 'info',
105+
});
106+
107+
telemetry.addBreadcrumb({
108+
type: 'custom',
109+
data: { id: 3 },
110+
timestamp: Date.now(),
111+
class: 'custom',
112+
level: 'info',
113+
});
114+
115+
const error = new Error('Test error');
116+
telemetry.captureError(error);
117+
telemetry.register(mockClient);
118+
119+
expect(mockClient.track).toHaveBeenCalledWith(
120+
'$ld:telemetry:error',
121+
expect.objectContaining({
122+
breadcrumbs: expect.arrayContaining([
123+
expect.objectContaining({ data: { id: 2 } }),
124+
expect.objectContaining({ data: { id: 3 } }),
125+
]),
126+
}),
127+
);
128+
});
129+
130+
it('handles null/undefined errors gracefully', () => {
131+
const telemetry = new BrowserTelemetryImpl(defaultOptions);
132+
133+
// @ts-ignore - Testing runtime behavior with invalid input
134+
telemetry.captureError(undefined);
135+
telemetry.register(mockClient);
136+
137+
expect(mockClient.track).toHaveBeenCalledWith(
138+
'$ld:telemetry:error',
139+
expect.objectContaining({
140+
type: 'generic',
141+
message: 'exception was null or undefined',
142+
breadcrumbs: [],
143+
}),
144+
);
145+
});
146+
147+
it('captures error events', () => {
148+
const telemetry = new BrowserTelemetryImpl(defaultOptions);
149+
const error = new Error('Test error');
150+
const errorEvent = new ErrorEvent('error', { error });
151+
152+
telemetry.captureErrorEvent(errorEvent);
153+
telemetry.register(mockClient);
154+
155+
expect(mockClient.track).toHaveBeenCalledWith(
156+
'$ld:telemetry:error',
157+
expect.objectContaining({
158+
type: 'Error',
159+
message: 'Test error',
160+
breadcrumbs: [],
161+
}),
162+
);
163+
});
164+
165+
it('handles flag evaluation breadcrumbs', () => {
166+
const telemetry = new BrowserTelemetryImpl(defaultOptions);
167+
168+
telemetry.handleFlagUsed('test-flag', {
169+
value: true,
170+
variationIndex: 1,
171+
reason: { kind: 'OFF' },
172+
});
173+
174+
const error = new Error('Test error');
175+
telemetry.captureError(error);
176+
telemetry.register(mockClient);
177+
178+
expect(mockClient.track).toHaveBeenCalledWith(
179+
'$ld:telemetry:error',
180+
expect.objectContaining({
181+
breadcrumbs: expect.arrayContaining([
182+
expect.objectContaining({
183+
type: 'flag-evaluated',
184+
data: {
185+
key: 'test-flag',
186+
value: true,
187+
},
188+
class: 'feature-management',
189+
}),
190+
]),
191+
}),
192+
);
193+
});
194+
195+
it('unregisters collectors on close', () => {
196+
const mockCollector = {
197+
register: jest.fn(),
198+
unregister: jest.fn(),
199+
};
200+
201+
const options: ParsedOptions = {
202+
...defaultOptions,
203+
collectors: [mockCollector],
204+
};
205+
206+
const telemetry = new BrowserTelemetryImpl(options);
207+
telemetry.close();
208+
209+
expect(mockCollector.unregister).toHaveBeenCalled();
210+
});

packages/telemetry/browser-telemetry/package.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,8 @@
4747
"rrweb": "2.0.0-alpha.4",
4848
"tracekit": "^0.4.6"
4949
},
50-
"peerDependencies": {
51-
"launchdarkly-js-client-sdk": "^3.4.0"
52-
},
5350
"devDependencies": {
51+
"@launchdarkly/js-client-sdk": "0.3.2",
5452
"@jest/globals": "^29.7.0",
5553
"@trivago/prettier-plugin-sort-imports": "^4.1.1",
5654
"@types/css-font-loading-module": "^0.0.13",

0 commit comments

Comments
 (0)