Skip to content

Commit d53b30c

Browse files
committed
WIP: Add makeIntegrationFn util
1 parent da8c9c3 commit d53b30c

File tree

4 files changed

+142
-4
lines changed

4 files changed

+142
-4
lines changed

packages/core/src/integration.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Client, Event, EventHint, Integration, Options } from '@sentry/types';
1+
import type { Client, Event, EventHint, Integration, IntegrationFn, IntegrationFnResult, Options } from '@sentry/types';
22
import { arrayify, logger } from '@sentry/utils';
33

44
import { DEBUG_BUILD } from './debug-build';
@@ -155,3 +155,27 @@ function findIndex<T>(arr: T[], callback: (item: T) => boolean): number {
155155

156156
return -1;
157157
}
158+
159+
/**
160+
* Generate a full integration function from a simple function.
161+
* This will ensure to add the given name both to the function definition, as well as to the integration return value.
162+
*/
163+
export function makeIntegrationFn<
164+
Fn extends (...rest: any[]) => Partial<IntegrationFnResult>,
165+
>(name: string, fn: Fn): ((...rest: Parameters<Fn>) => ReturnType<Fn> & { name: string }) & { id: string } {
166+
const patchedFn = addIdToIntegrationFnResult(name, fn);
167+
168+
169+
return Object.assign(patchedFn, { id: name });
170+
}
171+
172+
function addIdToIntegrationFnResult<Fn extends (...rest: any[]) => Partial<IntegrationFnResult>>(
173+
name: string,
174+
fn: Fn,
175+
): (...rest: Parameters<Fn>) => ReturnType<Fn> & { name: string } {
176+
const patchedFn = ((...rest: Parameters<Fn>): ReturnType<Fn> & { name: string } => {
177+
return { ...fn(...rest), name } as ReturnType<Fn> & { name: string };
178+
});
179+
180+
return patchedFn;
181+
}

packages/core/test/lib/integration.test.ts

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@ import type { Integration, Options } from '@sentry/types';
22
import { logger } from '@sentry/utils';
33

44
import { Hub, makeMain } from '../../src/hub';
5-
import { addIntegration, getIntegrationsToSetup, installedIntegrations, setupIntegration } from '../../src/integration';
5+
import {
6+
addIntegration,
7+
getIntegrationsToSetup,
8+
installedIntegrations,
9+
makeIntegrationFn,
10+
setupIntegration,
11+
} from '../../src/integration';
612
import { TestClient, getDefaultTestClientOptions } from '../mocks/client';
713

814
function getTestClient(): TestClient {
@@ -647,3 +653,69 @@ describe('addIntegration', () => {
647653
expect(warnings).toHaveBeenCalledWith('Cannot add integration "test" because no SDK Client is available.');
648654
});
649655
});
656+
657+
describe('makeIntegrationFn', () => {
658+
it('works with a minimal integration', () => {
659+
const myIntegration = makeIntegrationFn('testName', () => {
660+
return {};
661+
});
662+
663+
expect(myIntegration.id).toBe('testName');
664+
665+
const integration = myIntegration();
666+
expect(integration).toEqual({
667+
name: 'testName',
668+
});
669+
670+
// @ts-expect-error This SHOULD error
671+
myIntegration({});
672+
});
673+
674+
it('works with integration options', () => {
675+
const myIntegration = makeIntegrationFn('testName', (_options: { xxx: string }) => {
676+
return {};
677+
});
678+
679+
expect(myIntegration.id).toBe('testName');
680+
681+
const integration = myIntegration({ xxx: 'aa' });
682+
expect(integration).toEqual({
683+
name: 'testName',
684+
});
685+
686+
// @ts-expect-error This SHOULD error
687+
myIntegration();
688+
// @ts-expect-error This SHOULD error
689+
myIntegration({});
690+
});
691+
692+
it('works with integration hooks', () => {
693+
const setup = jest.fn();
694+
const setupOnce = jest.fn();
695+
const processEvent = jest.fn();
696+
const preprocessEvent = jest.fn();
697+
698+
const myIntegration = makeIntegrationFn('testName', () => {
699+
return {
700+
setup,
701+
setupOnce,
702+
processEvent,
703+
preprocessEvent,
704+
};
705+
});
706+
707+
expect(myIntegration.id).toBe('testName');
708+
709+
const integration = myIntegration();
710+
expect(integration).toEqual({
711+
name: 'testName',
712+
setup,
713+
setupOnce,
714+
processEvent,
715+
preprocessEvent,
716+
});
717+
718+
// @ts-expect-error This SHOULD error
719+
makeIntegrationFn('testName', () => ({ other: 'aha' }));
720+
});
721+
});

packages/types/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export type { EventProcessor } from './eventprocessor';
5252
export type { Exception } from './exception';
5353
export type { Extra, Extras } from './extra';
5454
export type { Hub } from './hub';
55-
export type { Integration, IntegrationClass } from './integration';
55+
export type { Integration, IntegrationClass, IntegrationFn, IntegrationFnResult } from './integration';
5656
export type { Mechanism } from './mechanism';
5757
export type { ExtractedNodeRequestData, HttpHeaderValue, Primitive, WorkerLocation } from './misc';
5858
export type { ClientOptions, Options } from './options';

packages/types/src/integration.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,49 @@ export interface IntegrationClass<T> {
1010
*/
1111
id: string;
1212

13-
new (...args: any[]): T;
13+
new(...args: any[]): T;
14+
}
15+
16+
/**
17+
* An integration in function form.
18+
* This is expected to return an integration result,
19+
* and also have a `name` property that holds the integration name (so we can e.g. filter these before actually calling them).
20+
*/
21+
export type IntegrationFn<Fn extends ((...rest: any[]) => IntegrationFnResult)> = { id: string } & Fn;
22+
23+
export interface IntegrationFnResult {
24+
/**
25+
* The name of the integration.
26+
*/
27+
name: string;
28+
29+
/**
30+
* This hook is only called once, even if multiple clients are created.
31+
* It does not receives any arguments, and should only use for e.g. global monkey patching and similar things.
32+
*/
33+
setupOnce?(): void;
34+
35+
/**
36+
* Set up an integration for the given client.
37+
* Receives the client as argument.
38+
*
39+
* Whenever possible, prefer this over `setupOnce`, as that is only run for the first client,
40+
* whereas `setup` runs for each client. Only truly global things (e.g. registering global handlers)
41+
* should be done in `setupOnce`.
42+
*/
43+
setup?(client: Client): void;
44+
45+
/**
46+
* An optional hook that allows to preprocess an event _before_ it is passed to all other event processors.
47+
*/
48+
preprocessEvent?(event: Event, hint: EventHint | undefined, client: Client): void;
49+
50+
/**
51+
* An optional hook that allows to process an event.
52+
* Return `null` to drop the event, or mutate the event & return it.
53+
* This receives the client that the integration was installed for as third argument.
54+
*/
55+
processEvent?(event: Event, hint: EventHint, client: Client): Event | null | PromiseLike<Event | null>;
1456
}
1557

1658
/** Integration interface */

0 commit comments

Comments
 (0)