Skip to content

Commit

Permalink
ref(deno): Refactor deno integrations to use functional syntax
Browse files Browse the repository at this point in the history
  • Loading branch information
mydea committed Dec 20, 2023
1 parent 812d10a commit a726c35
Show file tree
Hide file tree
Showing 5 changed files with 168 additions and 212 deletions.
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
101 changes: 48 additions & 53 deletions packages/deno/src/integrations/deno-cron.ts
Original file line number Diff line number Diff line change
@@ -1,61 +1,56 @@
import { withMonitor } from '@sentry/core';
import type { Integration } from '@sentry/types';
import type { DenoClient } from '../client';
import { convertIntegrationFnToClass, withMonitor } from '@sentry/core';
import type { 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 denoCronIntegration: IntegrationFn = () => {
return {
name: INTEGRATION_NAME,
setupOnce() {
// 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);
},
});
},
};
};

/** 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

0 comments on commit a726c35

Please sign in to comment.