Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ref(deno): Refactor deno integrations to use functional syntax #9929

Merged
merged 4 commits into from
Dec 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 16 additions & 19 deletions packages/deno/src/integrations/context.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import type { Event, EventProcessor, Integration } from '@sentry/types';
import { convertIntegrationFnToClass } from '@sentry/core';
import type { Event, IntegrationFn } from '@sentry/types';

const INTEGRATION_NAME = 'DenoContext';

function getOSName(): string {
switch (Deno.build.os) {
Expand All @@ -19,7 +22,7 @@ function getOSRelease(): string | undefined {
: undefined;
}

async function denoRuntime(event: Event): Promise<Event> {
async function addDenoRuntimeContext(event: Event): Promise<Event> {
event.contexts = {
...{
app: {
Expand Down Expand Up @@ -49,21 +52,15 @@ async function denoRuntime(event: Event): Promise<Event> {
return event;
}

/** Adds Electron context to events. */
export class DenoContext implements Integration {
/** @inheritDoc */
public static id = 'DenoContext';

/** @inheritDoc */
public name: string = DenoContext.id;

/** @inheritDoc */
public setupOnce(_addGlobalEventProcessor: (callback: EventProcessor) => void): void {
// noop
}
const denoContextIntegration: IntegrationFn = () => {
return {
name: INTEGRATION_NAME,
processEvent(event) {
return addDenoRuntimeContext(event);
},
};
};

/** @inheritDoc */
public processEvent(event: Event): Promise<Event> {
return denoRuntime(event);
}
}
/** Adds Deno context to events. */
// eslint-disable-next-line deprecation/deprecation
export const DenoContext = convertIntegrationFnToClass(INTEGRATION_NAME, denoContextIntegration);
105 changes: 44 additions & 61 deletions packages/deno/src/integrations/contextlines.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { Event, EventProcessor, Integration, StackFrame } from '@sentry/types';
import { convertIntegrationFnToClass } from '@sentry/core';
import type { Event, IntegrationFn, StackFrame } from '@sentry/types';
import { LRUMap, addContextToFrame } from '@sentry/utils';

const INTEGRATION_NAME = 'ContextLines';
const FILE_CONTENT_CACHE = new LRUMap<string, string | null>(100);
const DEFAULT_LINES_OF_CONTEXT = 7;

Expand Down Expand Up @@ -45,73 +47,54 @@ interface ContextLinesOptions {
frameContextLines?: number;
}

/** Add node modules / packages to the event */
export class ContextLines implements Integration {
/**
* @inheritDoc
*/
public static id = 'ContextLines';

/**
* @inheritDoc
*/
public name: string = ContextLines.id;

public constructor(private readonly _options: ContextLinesOptions = {}) {}

/** Get's the number of context lines to add */
private get _contextLines(): number {
return this._options.frameContextLines !== undefined ? this._options.frameContextLines : DEFAULT_LINES_OF_CONTEXT;
}

/**
* @inheritDoc
*/
public setupOnce(_addGlobalEventProcessor: (callback: EventProcessor) => void): void {
// noop
}
const denoContextLinesIntegration: IntegrationFn = (options: ContextLinesOptions = {}) => {
const contextLines = options.frameContextLines !== undefined ? options.frameContextLines : DEFAULT_LINES_OF_CONTEXT;

/** @inheritDoc */
public processEvent(event: Event): Promise<Event> {
return this.addSourceContext(event);
}
return {
name: INTEGRATION_NAME,
processEvent(event) {
return addSourceContext(event, contextLines);
},
};
};

/** Processes an event and adds context lines */
public async addSourceContext(event: Event): Promise<Event> {
if (this._contextLines > 0 && event.exception && event.exception.values) {
for (const exception of event.exception.values) {
if (exception.stacktrace && exception.stacktrace.frames) {
await this.addSourceContextToFrames(exception.stacktrace.frames);
}
/** Add node modules / packages to the event */
// eslint-disable-next-line deprecation/deprecation
export const ContextLines = convertIntegrationFnToClass(INTEGRATION_NAME, denoContextLinesIntegration);

/** Processes an event and adds context lines */
async function addSourceContext(event: Event, contextLines: number): Promise<Event> {
if (contextLines > 0 && event.exception && event.exception.values) {
for (const exception of event.exception.values) {
if (exception.stacktrace && exception.stacktrace.frames) {
await addSourceContextToFrames(exception.stacktrace.frames, contextLines);
}
}

return event;
}

/** Adds context lines to frames */
public async addSourceContextToFrames(frames: StackFrame[]): Promise<void> {
const contextLines = this._contextLines;

for (const frame of frames) {
// Only add context if we have a filename and it hasn't already been added
if (frame.filename && frame.in_app && frame.context_line === undefined) {
const permission = await Deno.permissions.query({
name: 'read',
path: frame.filename,
});

if (permission.state == 'granted') {
const sourceFile = await readSourceFile(frame.filename);
return event;
}

if (sourceFile) {
try {
const lines = sourceFile.split('\n');
addContextToFrame(lines, frame, contextLines);
} catch (_) {
// anomaly, being defensive in case
// unlikely to ever happen in practice but can definitely happen in theory
}
/** Adds context lines to frames */
async function addSourceContextToFrames(frames: StackFrame[], contextLines: number): Promise<void> {
for (const frame of frames) {
// Only add context if we have a filename and it hasn't already been added
if (frame.filename && frame.in_app && frame.context_line === undefined) {
const permission = await Deno.permissions.query({
name: 'read',
path: frame.filename,
});

if (permission.state == 'granted') {
const sourceFile = await readSourceFile(frame.filename);

if (sourceFile) {
try {
const lines = sourceFile.split('\n');
addContextToFrame(lines, frame, contextLines);
} catch (_) {
// anomaly, being defensive in case
// unlikely to ever happen in practice but can definitely happen in theory
}
}
}
Expand Down
110 changes: 57 additions & 53 deletions packages/deno/src/integrations/deno-cron.ts
Original file line number Diff line number Diff line change
@@ -1,61 +1,65 @@
import { withMonitor } from '@sentry/core';
import type { Integration } from '@sentry/types';
import type { DenoClient } from '../client';
import { convertIntegrationFnToClass, getClient, withMonitor } from '@sentry/core';
import type { Client, IntegrationFn } from '@sentry/types';
import { parseScheduleToString } from './deno-cron-format';

type CronOptions = { backoffSchedule?: number[]; signal?: AbortSignal };
type CronFn = () => void | Promise<void>;
// Parameters<typeof Deno.cron> doesn't work well with the overloads 🤔
type CronParams = [string, string | Deno.CronSchedule, CronFn | CronOptions, CronFn | CronOptions | undefined];

const INTEGRATION_NAME = 'DenoCron';

const SETUP_CLIENTS: Client[] = [];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This means that clients dont get GC'd - I wonder if we should track with a WeakMap instead?


const denoCronIntegration = (() => {
return {
name: INTEGRATION_NAME,
setupOnce() {
mydea marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AbhiPrasad / @timfish , ok, hopefully final refactor here - now this registers once in setupOnce, and registers the client in setup, which we check for in here!

// eslint-disable-next-line deprecation/deprecation
if (!Deno.cron) {
// The cron API is not available in this Deno version use --unstable flag!
return;
}

// eslint-disable-next-line deprecation/deprecation
Deno.cron = new Proxy(Deno.cron, {
apply(target, thisArg, argArray: CronParams) {
const [monitorSlug, schedule, opt1, opt2] = argArray;
let options: CronOptions | undefined;
let fn: CronFn;

if (typeof opt1 === 'function' && typeof opt2 !== 'function') {
fn = opt1;
options = opt2;
} else if (typeof opt1 !== 'function' && typeof opt2 === 'function') {
fn = opt2;
options = opt1;
}

async function cronCalled(): Promise<void> {
if (SETUP_CLIENTS.includes(getClient() as Client)) {
return;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we still want to call fn here?

}

await withMonitor(monitorSlug, async () => fn(), {
schedule: { type: 'crontab', value: parseScheduleToString(schedule) },
// (minutes) so 12 hours - just a very high arbitrary number since we don't know the actual duration of the users cron job
maxRuntime: 60 * 12,
// Deno Deploy docs say that the cron job will be called within 1 minute of the scheduled time
checkinMargin: 1,
});
}

return target.call(thisArg, monitorSlug, schedule, options || {}, cronCalled);
},
});
},
setup(client) {
SETUP_CLIENTS.push(client);
},
};
}) satisfies IntegrationFn;

/** Instruments Deno.cron to automatically capture cron check-ins */
export class DenoCron implements Integration {
/** @inheritDoc */
public static id = 'DenoCron';

/** @inheritDoc */
public name: string = DenoCron.id;

/** @inheritDoc */
public setupOnce(): void {
//
}

/** @inheritDoc */
public setup(): void {
// eslint-disable-next-line deprecation/deprecation
if (!Deno.cron) {
// The cron API is not available in this Deno version use --unstable flag!
return;
}

// eslint-disable-next-line deprecation/deprecation
Deno.cron = new Proxy(Deno.cron, {
apply(target, thisArg, argArray: CronParams) {
const [monitorSlug, schedule, opt1, opt2] = argArray;
let options: CronOptions | undefined;
let fn: CronFn;

if (typeof opt1 === 'function' && typeof opt2 !== 'function') {
fn = opt1;
options = opt2;
} else if (typeof opt1 !== 'function' && typeof opt2 === 'function') {
fn = opt2;
options = opt1;
}

async function cronCalled(): Promise<void> {
await withMonitor(monitorSlug, async () => fn(), {
schedule: { type: 'crontab', value: parseScheduleToString(schedule) },
// (minutes) so 12 hours - just a very high arbitrary number since we don't know the actual duration of the users cron job
maxRuntime: 60 * 12,
// Deno Deploy docs say that the cron job will be called within 1 minute of the scheduled time
checkinMargin: 1,
});
}

return target.call(thisArg, monitorSlug, schedule, options || {}, cronCalled);
},
});
}
}
// eslint-disable-next-line deprecation/deprecation
export const DenoCron = convertIntegrationFnToClass(INTEGRATION_NAME, denoCronIntegration);
66 changes: 25 additions & 41 deletions packages/deno/src/integrations/globalhandlers.ts
Original file line number Diff line number Diff line change
@@ -1,57 +1,41 @@
import type { ServerRuntimeClient } from '@sentry/core';
import { convertIntegrationFnToClass } from '@sentry/core';
import { captureEvent } from '@sentry/core';
import { getClient } from '@sentry/core';
import { flush } from '@sentry/core';
import type { Client, Event, Integration, Primitive, StackParser } from '@sentry/types';
import type { Client, Event, IntegrationFn, Primitive, StackParser } from '@sentry/types';
import { eventFromUnknownInput, isPrimitive } from '@sentry/utils';

type GlobalHandlersIntegrationsOptionKeys = 'error' | 'unhandledrejection';

/** JSDoc */
type GlobalHandlersIntegrations = Record<GlobalHandlersIntegrationsOptionKeys, boolean>;

const INTEGRATION_NAME = 'GlobalHandlers';
let isExiting = false;

/** Global handlers */
export class GlobalHandlers implements Integration {
/**
* @inheritDoc
*/
public static id = 'GlobalHandlers';

/**
* @inheritDoc
*/
public name: string = GlobalHandlers.id;

/** JSDoc */
private readonly _options: GlobalHandlersIntegrations;

/** JSDoc */
public constructor(options?: GlobalHandlersIntegrations) {
this._options = {
error: true,
unhandledrejection: true,
...options,
};
}
/**
* @inheritDoc
*/
public setupOnce(): void {
// noop
}
const globalHandlersIntegration: IntegrationFn = (options?: GlobalHandlersIntegrations) => {
const _options = {
error: true,
unhandledrejection: true,
...options,
};

/** @inheritdoc */
public setup(client: Client): void {
if (this._options.error) {
installGlobalErrorHandler(client);
}
if (this._options.unhandledrejection) {
installGlobalUnhandledRejectionHandler(client);
}
}
}
return {
name: INTEGRATION_NAME,
setup(client) {
if (_options.error) {
installGlobalErrorHandler(client);
}
if (_options.unhandledrejection) {
installGlobalUnhandledRejectionHandler(client);
}
},
};
};

/** Global handlers */
// eslint-disable-next-line deprecation/deprecation
export const GlobalHandlers = convertIntegrationFnToClass(INTEGRATION_NAME, globalHandlersIntegration);

function installGlobalErrorHandler(client: Client): void {
globalThis.addEventListener('error', data => {
Expand Down
Loading