Skip to content

Commit 8b0ee06

Browse files
committed
2 parents a1e327b + 2ef1486 commit 8b0ee06

File tree

20 files changed

+2526
-32
lines changed

20 files changed

+2526
-32
lines changed

packages/sdk/browser/tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
"rootDir": ".",
1212
"outDir": "dist",
1313
"skipLibCheck": true,
14-
"sourceMap": false,
14+
"sourceMap": true,
15+
"inlineSources": true,
1516
"strict": true,
1617
"stripInternal": true,
1718
"target": "ES2017",

packages/sdk/browser/tsup.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export default defineConfig({
1010
minify: true,
1111
format: ['esm', 'cjs'],
1212
splitting: false,
13-
sourcemap: false,
13+
sourcemap: true,
1414
clean: true,
1515
noExternal: ['@launchdarkly/js-sdk-common', '@launchdarkly/js-client-sdk-common'],
1616
dts: true,

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

Lines changed: 314 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { LDClientLogging } from '../src/api';
12
import { LDClientTracking } from '../src/api/client/LDClientTracking';
23
import BrowserTelemetryImpl from '../src/BrowserTelemetryImpl';
34
import { ParsedOptions } from '../src/options';
@@ -22,6 +23,7 @@ const defaultOptions: ParsedOptions = {
2223
},
2324
evaluations: true,
2425
flagChange: true,
26+
filters: [],
2527
},
2628
stack: {
2729
source: {
@@ -208,3 +210,315 @@ it('unregisters collectors on close', () => {
208210

209211
expect(mockCollector.unregister).toHaveBeenCalled();
210212
});
213+
214+
it('logs event dropped message when maxPendingEvents is reached', () => {
215+
const mockLogger = {
216+
warn: jest.fn(),
217+
};
218+
const telemetry = new BrowserTelemetryImpl({
219+
...defaultOptions,
220+
maxPendingEvents: 2,
221+
logger: mockLogger,
222+
});
223+
telemetry.captureError(new Error('Test error'));
224+
expect(mockLogger.warn).not.toHaveBeenCalled();
225+
telemetry.captureError(new Error('Test error 2'));
226+
expect(mockLogger.warn).not.toHaveBeenCalled();
227+
228+
telemetry.captureError(new Error('Test error 3'));
229+
expect(mockLogger.warn).toHaveBeenCalledWith(
230+
'LaunchDarkly - Browser Telemetry: Maximum pending events reached. Old events will be dropped until the SDK' +
231+
' client is registered.',
232+
);
233+
234+
telemetry.captureError(new Error('Test error 4'));
235+
expect(mockLogger.warn).toHaveBeenCalledTimes(1);
236+
});
237+
238+
it('filters breadcrumbs using provided filters', () => {
239+
const options: ParsedOptions = {
240+
...defaultOptions,
241+
breadcrumbs: {
242+
...defaultOptions.breadcrumbs,
243+
click: false,
244+
evaluations: false,
245+
flagChange: false,
246+
http: { instrumentFetch: false, instrumentXhr: false },
247+
keyboardInput: false,
248+
filters: [
249+
// Filter to remove breadcrumbs with id:2
250+
(breadcrumb) => {
251+
if (breadcrumb.type === 'custom' && breadcrumb.data?.id === 2) {
252+
return undefined;
253+
}
254+
return breadcrumb;
255+
},
256+
// Filter to transform breadcrumbs with id:3
257+
(breadcrumb) => {
258+
if (breadcrumb.type === 'custom' && breadcrumb.data?.id === 3) {
259+
return {
260+
...breadcrumb,
261+
data: { id: 'filtered-3' },
262+
};
263+
}
264+
return breadcrumb;
265+
},
266+
],
267+
},
268+
};
269+
const telemetry = new BrowserTelemetryImpl(options);
270+
271+
telemetry.addBreadcrumb({
272+
type: 'custom',
273+
data: { id: 1 },
274+
timestamp: Date.now(),
275+
class: 'custom',
276+
level: 'info',
277+
});
278+
279+
telemetry.addBreadcrumb({
280+
type: 'custom',
281+
data: { id: 2 },
282+
timestamp: Date.now(),
283+
class: 'custom',
284+
level: 'info',
285+
});
286+
287+
telemetry.addBreadcrumb({
288+
type: 'custom',
289+
data: { id: 3 },
290+
timestamp: Date.now(),
291+
class: 'custom',
292+
level: 'info',
293+
});
294+
295+
const error = new Error('Test error');
296+
telemetry.captureError(error);
297+
telemetry.register(mockClient);
298+
299+
expect(mockClient.track).toHaveBeenCalledWith(
300+
'$ld:telemetry:error',
301+
expect.objectContaining({
302+
breadcrumbs: expect.arrayContaining([
303+
expect.objectContaining({ data: { id: 1 } }),
304+
expect.objectContaining({ data: { id: 'filtered-3' } }),
305+
]),
306+
}),
307+
);
308+
309+
// Verify breadcrumb with id:2 was filtered out
310+
expect(mockClient.track).toHaveBeenCalledWith(
311+
'$ld:telemetry:error',
312+
expect.objectContaining({
313+
breadcrumbs: expect.not.arrayContaining([expect.objectContaining({ data: { id: 2 } })]),
314+
}),
315+
);
316+
});
317+
318+
it('omits breadcrumb when a filter throws an exception', () => {
319+
const breadSpy = jest.fn((breadcrumb) => breadcrumb);
320+
const options: ParsedOptions = {
321+
...defaultOptions,
322+
breadcrumbs: {
323+
...defaultOptions.breadcrumbs,
324+
filters: [
325+
() => {
326+
throw new Error('Filter error');
327+
},
328+
// This filter should never run
329+
breadSpy,
330+
],
331+
},
332+
};
333+
const telemetry = new BrowserTelemetryImpl(options);
334+
335+
telemetry.addBreadcrumb({
336+
type: 'custom',
337+
data: { id: 1 },
338+
timestamp: Date.now(),
339+
class: 'custom',
340+
level: 'info',
341+
});
342+
343+
const error = new Error('Test error');
344+
telemetry.captureError(error);
345+
telemetry.register(mockClient);
346+
347+
expect(mockClient.track).toHaveBeenCalledWith(
348+
'$ld:telemetry:error',
349+
expect.objectContaining({
350+
breadcrumbs: [],
351+
}),
352+
);
353+
354+
expect(breadSpy).not.toHaveBeenCalled();
355+
});
356+
357+
it('omits breadcrumbs when a filter is not a function', () => {
358+
const options: ParsedOptions = {
359+
...defaultOptions,
360+
breadcrumbs: {
361+
...defaultOptions.breadcrumbs,
362+
// @ts-ignore
363+
filters: ['potato'],
364+
},
365+
};
366+
const telemetry = new BrowserTelemetryImpl(options);
367+
368+
telemetry.addBreadcrumb({
369+
type: 'custom',
370+
data: { id: 1 },
371+
timestamp: Date.now(),
372+
class: 'custom',
373+
level: 'info',
374+
});
375+
376+
const error = new Error('Test error');
377+
telemetry.captureError(error);
378+
telemetry.register(mockClient);
379+
380+
expect(mockClient.track).toHaveBeenCalledWith(
381+
'$ld:telemetry:error',
382+
expect.objectContaining({
383+
breadcrumbs: [],
384+
}),
385+
);
386+
});
387+
388+
it('warns when a breadcrumb filter is not a function', () => {
389+
const mockLogger = {
390+
warn: jest.fn(),
391+
};
392+
const options: ParsedOptions = {
393+
...defaultOptions,
394+
// @ts-ignore
395+
breadcrumbs: { ...defaultOptions.breadcrumbs, filters: ['potato'] },
396+
logger: mockLogger,
397+
};
398+
399+
const telemetry = new BrowserTelemetryImpl(options);
400+
telemetry.addBreadcrumb({
401+
type: 'custom',
402+
data: { id: 1 },
403+
timestamp: Date.now(),
404+
class: 'custom',
405+
level: 'info',
406+
});
407+
408+
expect(mockLogger.warn).toHaveBeenCalledWith(
409+
'LaunchDarkly - Browser Telemetry: Error applying breadcrumb filters: TypeError: filter is not a function',
410+
);
411+
});
412+
413+
it('warns when a breadcrumb filter throws an exception', () => {
414+
const mockLogger = {
415+
warn: jest.fn(),
416+
};
417+
const options: ParsedOptions = {
418+
...defaultOptions,
419+
breadcrumbs: {
420+
...defaultOptions.breadcrumbs,
421+
filters: [
422+
() => {
423+
throw new Error('Filter error');
424+
},
425+
],
426+
},
427+
logger: mockLogger,
428+
};
429+
430+
const telemetry = new BrowserTelemetryImpl(options);
431+
telemetry.addBreadcrumb({
432+
type: 'custom',
433+
data: { id: 1 },
434+
timestamp: Date.now(),
435+
class: 'custom',
436+
level: 'info',
437+
});
438+
439+
expect(mockLogger.warn).toHaveBeenCalledWith(
440+
'LaunchDarkly - Browser Telemetry: Error applying breadcrumb filters: Error: Filter error',
441+
);
442+
});
443+
444+
it('only logs breadcrumb filter error once', () => {
445+
const mockLogger = {
446+
warn: jest.fn(),
447+
};
448+
const options: ParsedOptions = {
449+
...defaultOptions,
450+
breadcrumbs: {
451+
...defaultOptions.breadcrumbs,
452+
filters: [
453+
() => {
454+
throw new Error('Filter error');
455+
},
456+
],
457+
},
458+
logger: mockLogger,
459+
};
460+
461+
const telemetry = new BrowserTelemetryImpl(options);
462+
463+
// Add multiple breadcrumbs that will trigger the filter error
464+
telemetry.addBreadcrumb({
465+
type: 'custom',
466+
data: { id: 1 },
467+
timestamp: Date.now(),
468+
class: 'custom',
469+
level: 'info',
470+
});
471+
472+
telemetry.addBreadcrumb({
473+
type: 'custom',
474+
data: { id: 2 },
475+
timestamp: Date.now(),
476+
class: 'custom',
477+
level: 'info',
478+
});
479+
480+
// Verify warning was only logged once
481+
expect(mockLogger.warn).toHaveBeenCalledTimes(1);
482+
expect(mockLogger.warn).toHaveBeenCalledWith(
483+
'LaunchDarkly - Browser Telemetry: Error applying breadcrumb filters: Error: Filter error',
484+
);
485+
});
486+
487+
it('uses the client logger when no logger is provided', () => {
488+
const options: ParsedOptions = {
489+
...defaultOptions,
490+
breadcrumbs: {
491+
...defaultOptions.breadcrumbs,
492+
filters: [
493+
() => {
494+
throw new Error('Filter error');
495+
},
496+
],
497+
},
498+
};
499+
500+
const telemetry = new BrowserTelemetryImpl(options);
501+
502+
const mockClientWithLogging: jest.Mocked<LDClientLogging & LDClientTracking> = {
503+
logger: {
504+
warn: jest.fn(),
505+
},
506+
track: jest.fn(),
507+
};
508+
509+
telemetry.register(mockClientWithLogging);
510+
511+
// Add multiple breadcrumbs that will trigger the filter error
512+
telemetry.addBreadcrumb({
513+
type: 'custom',
514+
data: { id: 1 },
515+
timestamp: Date.now(),
516+
class: 'custom',
517+
level: 'info',
518+
});
519+
520+
expect(mockClientWithLogging.logger.warn).toHaveBeenCalledTimes(1);
521+
expect(mockClientWithLogging.logger.warn).toHaveBeenCalledWith(
522+
'LaunchDarkly - Browser Telemetry: Error applying breadcrumb filters: Error: Filter error',
523+
);
524+
});

0 commit comments

Comments
 (0)