Skip to content

Commit

Permalink
refactor(ConsoleInstrumentation): Provide config option to send log m…
Browse files Browse the repository at this point in the history
…essages for console.error calls (#731)
  • Loading branch information
codecapitano authored Nov 14, 2024
1 parent fb09bfc commit f70c473
Show file tree
Hide file tree
Showing 6 changed files with 285 additions and 9 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
- Fix (`@grafana/faro-web-sdk`): Prevents circular references in objects sent via `console.error`
messages (#730)

- Refactor (`@grafana/faro-web-sdk`): Provide config option to send log messages for console.error
calls (#731)

- Feat (`@grafana/faro-web-sdk`): Provide a `getIgnoreUrls()` function to easily retrieve the
configured ignoreUrls (#732)

Expand Down
135 changes: 135 additions & 0 deletions packages/core/src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,47 +4,182 @@ import type { InternalLoggerLevel } from '../internalLogger';
import type { Meta, MetaApp, MetaItem, MetaSession, MetaUser, MetaView } from '../metas';
import type { BatchExecutorOptions, BeforeSendHook, Transport } from '../transports';
import type { UnpatchedConsole } from '../unpatchedConsole';
import type { LogLevel } from '../utils';

type SamplingContext = {
metas: Meta;
};

export interface Config<P = APIEvent> {
/**
* Application metadata
*/
app: MetaApp;

/**
* Set max number and max interval for signals to be batched before sending
*/
batching?: BatchExecutorOptions;

/**
* A flag for toggling deduplication for signals
*/
dedupe: boolean;

/**
* The key (name) to use for the global Faro object (default: 'faro')
*/
globalObjectKey: string;

/**
* The (custom) instrumentations to use with Faro
*/
instrumentations: Instrumentation[];

/**
* The level of information printed to console for internal messages (default: LogLevel.ERROR)
*/
internalLoggerLevel: InternalLoggerLevel;

/**
* Isolate Faro instance from other Faro instances on the same page. (default: false)
*/
isolate: boolean;

/**
* Custom function to serialize log arguments
*/
logArgsSerializer?: LogArgsSerializer;

/**
* Add custom Metas during Faro initialization
*/
metas: MetaItem[];

/**
* Custom function used to parse stack traces
*/
parseStacktrace: StacktraceParser;

/**
* Pause sending data (default: false)
*/
paused: boolean;

/**
* Prevent Faro from exposing itself to the global object (default: false)
*/
preventGlobalExposure: boolean;

/**
* The transports to use for sending beacons
*/
transports: Transport[];

/**
* Some instrumentations might override the default console methods but Faro instance provides a
* way to access the unmodified console methods.
*
* faro.unpatchedConsole.log('This is a log');
* faro.unpatchedConsole.warn('This is a warning');
*/
unpatchedConsole: UnpatchedConsole;

/**
* Function which invoked before pushing event to transport. Can be used to modify or filter events
*/
beforeSend?: BeforeSendHook<P>;

/**
* Error message patterns for errors that should be ignored
*/
ignoreErrors?: Patterns;

/**
* Path patterns for Endpoints that should be ignored form being tracked
*/
ignoreUrls?: Patterns;

/**
* Configuration for the built in session tracker
*/
sessionTracking?: {
/**
* Enable session tracking (default: true)
*/
enabled?: boolean;
/**
* Wether to use sticky sessions (default: false)
*/
persistent?: boolean;
/**
* Session metadata object to be used when initializing session tracking
*/
session?: MetaSession;
/**
* How long is a sticky session valid for recurring users (default: 15 minutes)
*/
maxSessionPersistenceTime?: number;
/**
* Called each time a session changes. This can be when a new session is created or when an existing session is updated.
* @param oldSession
* @param newSession
*/
onSessionChange?: (oldSession: MetaSession | null, newSession: MetaSession) => void;
/**
* Then sampling rate for the session based sampler (default: 1). If a session is not part of a sample, no signals for this session are tracked.
*/
samplingRate?: number;
/**
* Custom sampler function if custom sampling logic is needed.
* @param context
*/
sampler?: (context: SamplingContext) => number;
/**
* Custom function to generate session id. If available Faro uses this function instead of the internal one.
*/
generateSessionId?: () => string;
};

/**
* Meta object for user data
*/
user?: MetaUser;

/**
* Meta object for view data
*/
view?: MetaView;

eventDomain?: string;

/**
* Only resource timings for fetch and xhr requests are tracked by default. Set this to true to track all resources (default: false).
*/
trackResources?: boolean;

/**
* Track web vitals attribution data (default: false)
*/
trackWebVitalsAttribution?: boolean;

/**
* Configuration for the console instrumentation
*/
consoleInstrumentation?: {
/**
* Configure what console levels should be captured by Faro. By default the follwoing levels
* are disabled: console.debug, console.trace, console.log
*
* If you want to collect all levels set captureConsoleDisabledLevels: [];
* If you want to disable only some levels set captureConsoleDisabledLevels: [LogLevel.DEBUG, LogLevel.TRACE];
*/
disabledLevels?: LogLevel[];
/*
* By default, Faro sends an error for console.error calls. If you want to send a log instead, set this to true.
*/
consoleErrorAsLog?: boolean;
};
}

export type Patterns = Array<string | RegExp>;
1 change: 1 addition & 0 deletions packages/web-sdk/src/config/makeCoreConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export function makeCoreConfig(browserConfig: BrowserConfig): Config | undefined
view: browserConfig.view,
trackResources: browserConfig.trackResources,
trackWebVitalsAttribution: browserConfig.trackWebVitalsAttribution,
consoleInstrumentation: browserConfig.consoleInstrumentation,
};

return config;
Expand Down
133 changes: 131 additions & 2 deletions packages/web-sdk/src/instrumentations/console/instrumentation.test.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,32 @@
import { ExceptionEvent, initializeFaro, TransportItem } from '@grafana/faro-core';
import { initializeFaro, LogLevel, TransportItem } from '@grafana/faro-core';
import type { ExceptionEvent, LogEvent } from '@grafana/faro-core';
import { mockConfig, MockTransport } from '@grafana/faro-core/src/testUtils';

import { makeCoreConfig } from '../../config';

import { ConsoleInstrumentation } from './instrumentation';

describe('ConsoleInstrumentation', () => {
const originalConsole = console;

beforeEach(() => {
global.console = {
error: jest.fn(),
log: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
trace: jest.fn(),
debug: jest.fn(),
} as unknown as Console;
});

afterEach(() => {
jest.resetAllMocks();
jest.restoreAllMocks();
global.console = originalConsole;
});

it('send a faro error when console.error is called', () => {
it('sends a faro error when console.error is called', () => {
const mockTransport = new MockTransport();

initializeFaro(
Expand Down Expand Up @@ -65,4 +80,118 @@ describe('ConsoleInstrumentation', () => {
'console.error: with circular refs object {"foo":"bar","baz":"bam","circular":null}'
);
});

it('sends a faro log for console.error calls if configured', () => {
const mockTransport = new MockTransport();

initializeFaro(
makeCoreConfig(
mockConfig({
transports: [mockTransport],
instrumentations: [new ConsoleInstrumentation()],
unpatchedConsole: {
error: jest.fn(),
} as unknown as Console,
consoleInstrumentation: {
consoleErrorAsLog: true,
},
})
)!
);

console.error('console.error log no 1');
console.error('console.error log with object', { foo: 'bar', baz: 'bam' });

expect(mockTransport.items).toHaveLength(2);

expect((mockTransport.items[0] as TransportItem<LogEvent>)?.payload.message).toBe('console.error log no 1');
expect((mockTransport.items[1] as TransportItem<LogEvent>)?.payload.message).toBe(
'console.error log with object {"foo":"bar","baz":"bam"}'
);
});

it('Uses legacy config options', () => {
const mockTransport = new MockTransport();
initializeFaro(
makeCoreConfig(
mockConfig({
transports: [mockTransport],
instrumentations: [
new ConsoleInstrumentation({
consoleErrorAsLog: true,
disabledLevels: [LogLevel.LOG],
}),
],
unpatchedConsole: {
error: jest.fn(),
log: jest.fn(),
info: jest.fn(),
} as unknown as Console,
})
)!
);

console.error('error logs are enabled');
console.info('info logs are enabled');
console.log('log logs are disabled');

expect(mockTransport.items).toHaveLength(2);
expect((mockTransport.items[0] as TransportItem<LogEvent>)?.payload.message).toBe('error logs are enabled');
expect((mockTransport.items[1] as TransportItem<LogEvent>)?.payload.message).toBe('info logs are enabled');
});

it('sends logs for the default enabled event if no config is provided', () => {
const mockTransport = new MockTransport();

initializeFaro(
makeCoreConfig(
mockConfig({
transports: [mockTransport],
instrumentations: [new ConsoleInstrumentation()],
unpatchedConsole: {
error: jest.fn(),
log: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
trace: jest.fn(),
debug: jest.fn(),
} as unknown as Console,
})
)!
);

// included by default
const infoLogMessage = 'info is logged by default';
console.info(infoLogMessage);

const warnLogMessage = 'warn is logged by default';
console.warn(warnLogMessage);

const errorLogMessage = 'error is logged by default';
console.error(errorLogMessage);

const excludedLogMessage = "log isn't logged by default";

// excluded by default
console.log(excludedLogMessage);
const excludedTraceLogMessage = "trace isn't logged by default";
// eslint-disable-next-line no-console
console.trace(excludedTraceLogMessage);
const excludedDebugMessage = "debug isn't logged by default";
// eslint-disable-next-line no-console
console.debug(excludedDebugMessage);

expect(mockTransport.items).toHaveLength(3);

expect((mockTransport.items[0] as TransportItem<LogEvent>)?.payload.message).toBe(infoLogMessage);
expect((mockTransport.items[0] as TransportItem<LogEvent>)?.payload.level).toBe('info');

expect((mockTransport.items[1] as TransportItem<LogEvent>)?.payload.message).toBe(warnLogMessage);
expect((mockTransport.items[1] as TransportItem<LogEvent>)?.payload.level).toBe('warn');

// error is logged by default and is logged as an exception signal
expect((mockTransport.items[2] as TransportItem<ExceptionEvent>)?.payload.value).toBe(
'console.error: ' + errorLogMessage
);
});
});
12 changes: 9 additions & 3 deletions packages/web-sdk/src/instrumentations/console/instrumentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,28 @@ export class ConsoleInstrumentation extends BaseInstrumentation {

initialize() {
this.logDebug('Initializing\n', this.options);
this.options = { ...this.options, ...this.config.consoleInstrumentation };

allLogLevels
.filter((level) => !(this.options.disabledLevels ?? ConsoleInstrumentation.defaultDisabledLevels).includes(level))
.filter(
(level) => !(this.options?.disabledLevels ?? ConsoleInstrumentation.defaultDisabledLevels).includes(level)
)
.forEach((level) => {
/* eslint-disable-next-line no-console */
console[level] = (...args) => {
try {
if (level === LogLevel.ERROR) {
if (level === LogLevel.ERROR && !this.options?.consoleErrorAsLog) {
this.api.pushError(
new Error(
'console.error: ' +
args.map((arg) => (isObject(arg) || isArray(arg) ? stringifyExternalJson(arg) : arg)).join(' ')
)
);
} else {
this.api.pushLog(args, { level });
this.api.pushLog(
[args.map((arg) => (isObject(arg) || isArray(arg) ? JSON.stringify(arg) : arg)).join(' ')],
{ level }
);
}
} catch (err) {
this.logError(err);
Expand Down
10 changes: 6 additions & 4 deletions packages/web-sdk/src/instrumentations/console/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { LogLevel } from '@grafana/faro-core';
import type { Config } from '@grafana/faro-core';

export interface ConsoleInstrumentationOptions {
disabledLevels?: LogLevel[];
}
/**
* @deprecated Configure console instrumentation using the `consoleInstrumentation` object in the
* Faro config.
*/
export type ConsoleInstrumentationOptions = Config['consoleInstrumentation'];

0 comments on commit f70c473

Please sign in to comment.