From 1a3dc194377fc059dcae35a55792e6517822e8a3 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Thu, 30 Jan 2025 15:55:58 -0800 Subject: [PATCH] feat: Option to disable all breadcrumbs and stack. --- .../__tests__/BrowserTelemetryImpl.test.ts | 1 + .../__tests__/options.test.ts | 36 ++++ .../__tests__/stack/StackParser.test.ts | 19 +- .../src/BrowserTelemetryImpl.ts | 4 + .../browser-telemetry/src/api/Options.ts | 167 +++++++-------- .../browser-telemetry/src/options.ts | 199 +++++++++++------- .../src/stack/StackParser.ts | 6 + 7 files changed, 274 insertions(+), 158 deletions(-) diff --git a/packages/telemetry/browser-telemetry/__tests__/BrowserTelemetryImpl.test.ts b/packages/telemetry/browser-telemetry/__tests__/BrowserTelemetryImpl.test.ts index d9c1af07f1..fbdf135c2e 100644 --- a/packages/telemetry/browser-telemetry/__tests__/BrowserTelemetryImpl.test.ts +++ b/packages/telemetry/browser-telemetry/__tests__/BrowserTelemetryImpl.test.ts @@ -26,6 +26,7 @@ const defaultOptions: ParsedOptions = { filters: [], }, stack: { + enabled: true, source: { beforeLines: 5, afterLines: 5, diff --git a/packages/telemetry/browser-telemetry/__tests__/options.test.ts b/packages/telemetry/browser-telemetry/__tests__/options.test.ts index 84cd6afa3c..190205541e 100644 --- a/packages/telemetry/browser-telemetry/__tests__/options.test.ts +++ b/packages/telemetry/browser-telemetry/__tests__/options.test.ts @@ -16,6 +16,27 @@ it('handles an empty configuration', () => { expect(outOptions).toEqual(defaultOptions()); }); +it('disables all breadcrumb options when breadcrumbs is false', () => { + const outOptions = parse({ + breadcrumbs: false, + }); + + expect(outOptions.breadcrumbs).toEqual({ + maxBreadcrumbs: 0, + click: false, + evaluations: false, + flagChange: false, + keyboardInput: false, + http: { + instrumentFetch: false, + instrumentXhr: false, + customUrlFilter: undefined, + }, + filters: [], + }); + expect(mockLogger.warn).not.toHaveBeenCalled(); +}); + it('can set all options at once', () => { const breadcrumbFilter = (breadcrumb: Breadcrumb) => breadcrumb; const errorFilter = (error: ErrorData) => error; @@ -473,3 +494,18 @@ it('accepts valid error filters array', () => { expect(outOptions.errorFilters).toEqual(errorFilters); expect(mockLogger.warn).not.toHaveBeenCalled(); }); + +it('disables all stack options when stack is false', () => { + const outOptions = parse({ + stack: false, + }); + + expect(outOptions.stack).toEqual({ + source: { + beforeLines: 0, + afterLines: 0, + maxLineLength: 0, + }, + }); + expect(mockLogger.warn).not.toHaveBeenCalled(); +}); diff --git a/packages/telemetry/browser-telemetry/__tests__/stack/StackParser.test.ts b/packages/telemetry/browser-telemetry/__tests__/stack/StackParser.test.ts index 0fdcbd445a..ee65f8b9de 100644 --- a/packages/telemetry/browser-telemetry/__tests__/stack/StackParser.test.ts +++ b/packages/telemetry/browser-telemetry/__tests__/stack/StackParser.test.ts @@ -1,4 +1,4 @@ -import { +import parse, { getLines, getSrcLines, processUrlToFileName, @@ -58,6 +58,7 @@ describe('given an input stack frame', () => { it('can produce a full stack source in the output frame', () => { expect( getSrcLines(inputFrame, { + enabled: true, source: { beforeLines: 2, afterLines: 2, @@ -74,6 +75,7 @@ describe('given an input stack frame', () => { it('can trim all the lines', () => { expect( getSrcLines(inputFrame, { + enabled: true, source: { beforeLines: 2, afterLines: 2, @@ -90,6 +92,7 @@ describe('given an input stack frame', () => { it('can handle fewer input lines than the expected context', () => { expect( getSrcLines(inputFrame, { + enabled: true, source: { beforeLines: 3, afterLines: 3, @@ -106,6 +109,7 @@ describe('given an input stack frame', () => { it('can handle more input lines than the expected context', () => { expect( getSrcLines(inputFrame, { + enabled: true, source: { beforeLines: 1, afterLines: 1, @@ -119,3 +123,16 @@ describe('given an input stack frame', () => { }); }); }); + +it('returns an empty stack when stack parsing is disabled', () => { + expect( + parse(new Error('test'), { + enabled: false, + source: { + beforeLines: 1, + afterLines: 1, + maxLineLength: 280, + }, + }), + ).toEqual({ frames: [] }); +}); diff --git a/packages/telemetry/browser-telemetry/src/BrowserTelemetryImpl.ts b/packages/telemetry/browser-telemetry/src/BrowserTelemetryImpl.ts index 3e39254a33..05a90c9ccf 100644 --- a/packages/telemetry/browser-telemetry/src/BrowserTelemetryImpl.ts +++ b/packages/telemetry/browser-telemetry/src/BrowserTelemetryImpl.ts @@ -65,6 +65,10 @@ function applyFilter(item: T | undefined, filter: (item: T) => T | undefined) } function configureTraceKit(options: ParsedStackOptions) { + if (!options.enabled) { + return; + } + const TraceKit = getTraceKit(); // Include before + after + source line. // TraceKit only takes a total context size, so we have to over capture and then reduce the lines. diff --git a/packages/telemetry/browser-telemetry/src/api/Options.ts b/packages/telemetry/browser-telemetry/src/api/Options.ts index f894923918..c3c847579a 100644 --- a/packages/telemetry/browser-telemetry/src/api/Options.ts +++ b/packages/telemetry/browser-telemetry/src/api/Options.ts @@ -93,6 +93,86 @@ export interface StackOptions { }; } +export interface BreadcrumbsOptions { + /** + * Set the maximum number of breadcrumbs. Defaults to 50. + */ + maxBreadcrumbs?: number; + + /** + * True to enable automatic evaluation breadcrumbs. Defaults to true. + */ + evaluations?: boolean; + + /** + * True to enable flag change breadcrumbs. Defaults to true. + */ + flagChange?: boolean; + + /** + * True to enable click breadcrumbs. Defaults to true. + */ + click?: boolean; + + /** + * True to enable input breadcrumbs for keypresses. Defaults to true. + * + * Input breadcrumbs do not include entered text, just that text was entered. + */ + keyboardInput?: boolean; + + /** + * Controls instrumentation and breadcrumbs for HTTP requests. + * The default is to instrument XMLHttpRequests and fetch requests. + * + * `false` to disable all HTTP breadcrumbs and instrumentation. + * + * Example: + * ``` + * // This would instrument only XmlHttpRequests + * http: { + * instrumentFetch: false + * instrumentXhr: true + * } + * + * // Disable all HTTP instrumentation: + * http: false + * ``` + */ + http?: HttpBreadcrumbOptions | false; + + /** + * Custom breadcrumb filters. + * + * Can be used to redact or modify breadcrumbs. + * + * Example: + * ``` + * // We want to redact any click events that include the message 'sneaky-button' + * filters: [ + * (breadcrumb) => { + * if( + * breadcrumb.class === 'ui' && + * breadcrumb.type === 'click' && + * breadcrumb.message?.includes('sneaky-button') + * ) { + * return; + * } + * return breadcrumb; + * } + * ] + * ``` + * + * If you want to redact or modify URLs in breadcrumbs, then a urlFilter should be used. + * + * If any breadcrumb filters throw an exception while processing a breadcrumb, then that breadcrumb will be excluded. + * + * If any breadcrumbFilter cannot be executed, for example because it is not a function, then all breadcrumbs will + * be excluded. + */ + filters?: BreadcrumbFilter[]; +} + /** * Options for configuring browser telemetry. */ @@ -103,88 +183,11 @@ export interface Options { * events captured during initialization. */ maxPendingEvents?: number; + /** - * Properties related to automatic breadcrumb collection. + * Properties related to automatic breadcrumb collection, or `false` to disable automatic breadcrumbs. */ - breadcrumbs?: { - /** - * Set the maximum number of breadcrumbs. Defaults to 50. - */ - maxBreadcrumbs?: number; - - /** - * True to enable automatic evaluation breadcrumbs. Defaults to true. - */ - evaluations?: boolean; - - /** - * True to enable flag change breadcrumbs. Defaults to true. - */ - flagChange?: boolean; - - /** - * True to enable click breadcrumbs. Defaults to true. - */ - click?: boolean; - - /** - * True to enable input breadcrumbs for keypresses. Defaults to true. - * - * Input breadcrumbs do not include entered text, just that text was entered. - */ - keyboardInput?: boolean; - - /** - * Controls instrumentation and breadcrumbs for HTTP requests. - * The default is to instrument XMLHttpRequests and fetch requests. - * - * `false` to disable all HTTP breadcrumbs and instrumentation. - * - * Example: - * ``` - * // This would instrument only XmlHttpRequests - * http: { - * instrumentFetch: false - * instrumentXhr: true - * } - * - * // Disable all HTTP instrumentation: - * http: false - * ``` - */ - http?: HttpBreadcrumbOptions | false; - - /** - * Custom breadcrumb filters. - * - * Can be used to redact or modify breadcrumbs. - * - * Example: - * ``` - * // We want to redact any click events that include the message 'sneaky-button' - * filters: [ - * (breadcrumb) => { - * if( - * breadcrumb.class === 'ui' && - * breadcrumb.type === 'click' && - * breadcrumb.message?.includes('sneaky-button') - * ) { - * return; - * } - * return breadcrumb; - * } - * ] - * ``` - * - * If you want to redact or modify URLs in breadcrumbs, then a urlFilter should be used. - * - * If any breadcrumb filters throw an exception while processing a breadcrumb, then that breadcrumb will be excluded. - * - * If any breadcrumbFilter cannot be executed, for example because it is not a function, then all breadcrumbs will - * be excluded. - */ - filters?: BreadcrumbFilter[]; - }; + breadcrumbs?: BreadcrumbsOptions | false; /** * Additional, or custom, collectors. @@ -192,9 +195,9 @@ export interface Options { collectors?: Collector[]; /** - * Configuration that controls the capture of the stack trace. + * Configuration that controls the capture of the stack trace, or `false` to exclude stack frames from error events. */ - stack?: StackOptions; + stack?: StackOptions | false; /** * Logger to use for warnings. diff --git a/packages/telemetry/browser-telemetry/src/options.ts b/packages/telemetry/browser-telemetry/src/options.ts index ce6ab21e19..477c149775 100644 --- a/packages/telemetry/browser-telemetry/src/options.ts +++ b/packages/telemetry/browser-telemetry/src/options.ts @@ -2,6 +2,7 @@ import { Collector } from './api/Collector'; import { MinLogger } from './api/MinLogger'; import { BreadcrumbFilter, + BreadcrumbsOptions, ErrorDataFilter, HttpBreadcrumbOptions, Options, @@ -10,6 +11,29 @@ import { } from './api/Options'; import { fallbackLogger, prefixLog, safeMinLogger } from './logging'; +const disabledBreadcrumbs: ParsedBreadcrumbsOptions = { + maxBreadcrumbs: 0, + evaluations: false, + flagChange: false, + click: false, + keyboardInput: false, + http: { + instrumentFetch: false, + instrumentXhr: false, + customUrlFilter: undefined, + }, + filters: [], +}; + +const disabledStack: ParsedStackOptions = { + enabled: false, + source: { + beforeLines: 0, + afterLines: 0, + maxLineLength: 0, + }, +}; + export function defaultOptions(): ParsedOptions { return { breadcrumbs: { @@ -25,6 +49,7 @@ export function defaultOptions(): ParsedOptions { filters: [], }, stack: { + enabled: true, source: { beforeLines: 3, afterLines: 3, @@ -128,11 +153,16 @@ function parseLogger(options: Options): MinLogger | undefined { } function parseStack( - options: StackOptions | undefined, + options: StackOptions | false | undefined, defaults: ParsedStackOptions, logger?: MinLogger, ): ParsedStackOptions { + if (options === false) { + return disabledStack; + } return { + // Internal option not parsed from the options object. + enabled: true, source: { beforeLines: itemOrDefault( options?.source?.beforeLines, @@ -153,6 +183,51 @@ function parseStack( }; } +function parseBreadcrumbs( + options: BreadcrumbsOptions | false | undefined, + defaults: ParsedBreadcrumbsOptions, + logger: MinLogger | undefined, +): ParsedBreadcrumbsOptions { + if (options === false) { + return disabledBreadcrumbs; + } + return { + maxBreadcrumbs: itemOrDefault( + options?.maxBreadcrumbs, + defaults.maxBreadcrumbs, + checkBasic('number', 'breadcrumbs.maxBreadcrumbs', logger), + ), + evaluations: itemOrDefault( + options?.evaluations, + defaults.evaluations, + checkBasic('boolean', 'breadcrumbs.evaluations', logger), + ), + flagChange: itemOrDefault( + options?.flagChange, + defaults.flagChange, + checkBasic('boolean', 'breadcrumbs.flagChange', logger), + ), + click: itemOrDefault( + options?.click, + defaults.click, + checkBasic('boolean', 'breadcrumbs.click', logger), + ), + keyboardInput: itemOrDefault( + options?.keyboardInput, + defaults.keyboardInput, + checkBasic('boolean', 'breadcrumbs.keyboardInput', logger), + ), + http: parseHttp(options?.http, defaults.http, logger), + filters: itemOrDefault(options?.filters, defaults.filters, (item) => { + if (Array.isArray(item)) { + return true; + } + logger?.warn(wrongOptionType('breadcrumbs.filters', 'BreadcrumbFilter[]', typeof item)); + return false; + }), + }; +} + export default function parse(options: Options, logger?: MinLogger): ParsedOptions { const defaults = defaultOptions(); if (options.breadcrumbs) { @@ -162,42 +237,8 @@ export default function parse(options: Options, logger?: MinLogger): ParsedOptio checkBasic('object', 'stack', logger)(options.stack); } return { - breadcrumbs: { - maxBreadcrumbs: itemOrDefault( - options.breadcrumbs?.maxBreadcrumbs, - defaults.breadcrumbs.maxBreadcrumbs, - checkBasic('number', 'breadcrumbs.maxBreadcrumbs', logger), - ), - evaluations: itemOrDefault( - options.breadcrumbs?.evaluations, - defaults.breadcrumbs.evaluations, - checkBasic('boolean', 'breadcrumbs.evaluations', logger), - ), - flagChange: itemOrDefault( - options.breadcrumbs?.flagChange, - defaults.breadcrumbs.flagChange, - checkBasic('boolean', 'breadcrumbs.flagChange', logger), - ), - click: itemOrDefault( - options.breadcrumbs?.click, - defaults.breadcrumbs.click, - checkBasic('boolean', 'breadcrumbs.click', logger), - ), - keyboardInput: itemOrDefault( - options.breadcrumbs?.keyboardInput, - defaults.breadcrumbs.keyboardInput, - checkBasic('boolean', 'breadcrumbs.keyboardInput', logger), - ), - http: parseHttp(options.breadcrumbs?.http, defaults.breadcrumbs.http, logger), - filters: itemOrDefault(options.breadcrumbs?.filters, defaults.breadcrumbs.filters, (item) => { - if (Array.isArray(item)) { - return true; - } - logger?.warn(wrongOptionType('breadcrumbs.filters', 'BreadcrumbFilter[]', typeof item)); - return false; - }), - }, - stack: parseStack(options.stack, defaults.stack), + breadcrumbs: parseBreadcrumbs(options.breadcrumbs, defaults.breadcrumbs, logger), + stack: parseStack(options.stack, defaults.stack, logger), maxPendingEvents: itemOrDefault( options.maxPendingEvents, defaults.maxPendingEvents, @@ -249,6 +290,7 @@ export interface ParsedHttpOptions { * @internal */ export interface ParsedStackOptions { + enabled: boolean; source: { /** * The number of lines captured before the originating line. @@ -269,58 +311,65 @@ export interface ParsedStackOptions { } /** - * Internal type for parsed options. + * Internal type for parsed breadcrumbs options. * @internal */ -export interface ParsedOptions { +export interface ParsedBreadcrumbsOptions { /** - * The maximum number of pending events. Events may be captured before the LaunchDarkly - * SDK is initialized and these are stored until they can be sent. This only affects the - * events captured during initialization. + * Set the maximum number of breadcrumbs. Defaults to 50. */ - maxPendingEvents: number; + maxBreadcrumbs: number; + /** - * Properties related to automatic breadcrumb collection. + * True to enable automatic evaluation breadcrumbs. Defaults to true. */ - breadcrumbs: { - /** - * Set the maximum number of breadcrumbs. Defaults to 50. - */ - maxBreadcrumbs: number; + evaluations: boolean; - /** - * True to enable automatic evaluation breadcrumbs. Defaults to true. - */ - evaluations: boolean; + /** + * True to enable flag change breadcrumbs. Defaults to true. + */ + flagChange: boolean; - /** - * True to enable flag change breadcrumbs. Defaults to true. - */ - flagChange: boolean; + /** + * True to enable click breadcrumbs. Defaults to true. + */ + click: boolean; - /** - * True to enable click breadcrumbs. Defaults to true. - */ - click: boolean; + /** + * True to enable input breadcrumbs for keypresses. Defaults to true. + */ + keyboardInput?: boolean; - /** - * True to enable input breadcrumbs for keypresses. Defaults to true. - */ - keyboardInput?: boolean; + /** + * Settings for http instrumentation and breadcrumbs. + */ + http: ParsedHttpOptions; - /** - * Settings for http instrumentation and breadcrumbs. - */ - http: ParsedHttpOptions; + /** + * Custom breadcrumb filters. + */ + filters: BreadcrumbFilter[]; +} - /** - * Custom breadcrumb filters. - */ - filters: BreadcrumbFilter[]; - }; +/** + * Internal type for parsed options. + * @internal + */ +export interface ParsedOptions { + /** + * The maximum number of pending events. Events may be captured before the LaunchDarkly + * SDK is initialized and these are stored until they can be sent. This only affects the + * events captured during initialization. + */ + maxPendingEvents: number; + + /** + * Properties related to automatic breadcrumb collection, or `false` to disable automatic breadcrumbs. + */ + breadcrumbs: ParsedBreadcrumbsOptions; /** - * Settings which affect call stack capture. + * Settings which affect call stack capture, or `false` to exclude stack frames from error events . */ stack: ParsedStackOptions; diff --git a/packages/telemetry/browser-telemetry/src/stack/StackParser.ts b/packages/telemetry/browser-telemetry/src/stack/StackParser.ts index 73f7552946..d4563359ca 100644 --- a/packages/telemetry/browser-telemetry/src/stack/StackParser.ts +++ b/packages/telemetry/browser-telemetry/src/stack/StackParser.ts @@ -194,6 +194,12 @@ export function getSrcLines( * @returns The stack trace for the given error. */ export default function parse(error: Error, options: ParsedStackOptions): StackTrace { + if (!options.enabled) { + return { + frames: [], + }; + } + const parsed = getTraceKit().computeStackTrace(error); const frames: StackFrame[] = parsed.stack.reverse().map((inFrame) => ({ fileName: processUrlToFileName(inFrame.url, window.location.origin),