Skip to content

feat: Auto-discover integrations #3472

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

Closed
wants to merge 5 commits into from
Closed
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
4 changes: 2 additions & 2 deletions packages/angular/src/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { BrowserOptions, init as browserInit, SDK_VERSION } from '@sentry/browse
* Inits the Angular SDK
*/
export function init(options: BrowserOptions): void {
options._metadata = options._metadata || {};
options._metadata.sdk = {
options._internal = options._internal || {};
options._internal.sdk = {
name: 'sentry.javascript.angular',
packages: [
{
Expand Down
2 changes: 1 addition & 1 deletion packages/browser/src/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export class BrowserBackend extends BaseBackend<BrowserOptions> {
const transportOptions = {
...this._options.transportOptions,
dsn: this._options.dsn,
_metadata: this._options._metadata,
_sdk: this._options._internal?.sdk,
};

if (this._options.transport) {
Expand Down
4 changes: 2 additions & 2 deletions packages/browser/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ export class BrowserClient extends BaseClient<BrowserBackend, BrowserOptions> {
* @param options Configuration options for this SDK.
*/
public constructor(options: BrowserOptions = {}) {
options._metadata = options._metadata || {};
options._metadata.sdk = options._metadata.sdk || {
options._internal = options._internal || {};
options._internal.sdk = options._internal.sdk || {
name: 'sentry.javascript.browser',
packages: [
{
Expand Down
11 changes: 9 additions & 2 deletions packages/browser/src/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,16 @@ export const defaultIntegrations = [
* @see {@link BrowserOptions} for documentation on configuration options.
*/
export function init(options: BrowserOptions = {}): void {
if (options.defaultIntegrations === undefined) {
options.defaultIntegrations = defaultIntegrations;
options._internal = options._internal || {};

// Both `defaultIntegrations` and `discoverIntegrations` should be `boolean`, but we are stuck with this type
// for backwards compatibility at the momement.
if (options.defaultIntegrations !== false) {
options._internal.defaultIntegrations = Array.isArray(options.defaultIntegrations)
? options.defaultIntegrations
: defaultIntegrations;
}

if (options.release === undefined) {
const window = getGlobalObject<Window>();
// This supports the variable that sentry-webpack-plugin injects
Expand Down
2 changes: 1 addition & 1 deletion packages/browser/src/transports/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export abstract class BaseTransport implements Transport {
protected readonly _rateLimits: Record<string, Date> = {};

public constructor(public options: TransportOptions) {
this._api = new API(options.dsn, options._metadata);
this._api = new API(options.dsn, options._sdk);
// eslint-disable-next-line deprecation/deprecation
this.url = this._api.getStoreEndpointWithUrlEncodedAuth();
}
Expand Down
8 changes: 4 additions & 4 deletions packages/browser/test/unit/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ describe('SentryBrowser initialization', () => {
init({ dsn });

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const sdkData = (getCurrentHub().getClient() as any)._backend._transport._api.metadata?.sdk;
const sdkData = (getCurrentHub().getClient() as any)._backend._transport._api.sdkInfo;

expect(sdkData.name).to.equal('sentry.javascript.browser');
expect(sdkData.packages[0].name).to.equal('npm:@sentry/browser');
Expand All @@ -202,7 +202,7 @@ describe('SentryBrowser initialization', () => {
const client = new BrowserClient({ dsn });

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const sdkData = (client as any)._backend._transport._api.metadata?.sdk;
const sdkData = (client as any)._backend._transport._api.sdkInfo;

expect(sdkData.name).to.equal('sentry.javascript.browser');
expect(sdkData.packages[0].name).to.equal('npm:@sentry/browser');
Expand All @@ -216,7 +216,7 @@ describe('SentryBrowser initialization', () => {
init({
dsn,
// this would normally be set by the wrapper SDK in init()
_metadata: {
_internal: {
sdk: {
name: 'sentry.javascript.angular',
packages: [
Expand All @@ -231,7 +231,7 @@ describe('SentryBrowser initialization', () => {
});

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const sdkData = (getCurrentHub().getClient() as any)._backend._transport._api.metadata?.sdk;
const sdkData = (getCurrentHub().getClient() as any)._backend._transport._api.sdkInfo;

expect(sdkData.name).to.equal('sentry.javascript.angular');
expect(sdkData.packages[0].name).to.equal('npm:@sentry/angular');
Expand Down
8 changes: 4 additions & 4 deletions packages/core/src/api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DsnLike, SdkMetadata } from '@sentry/types';
import { DsnLike, SdkInfo } from '@sentry/types';
import { Dsn, urlEncode } from '@sentry/utils';

const SENTRY_API_VERSION = '7';
Expand All @@ -13,16 +13,16 @@ export class API {
public dsn: DsnLike;

/** Metadata about the SDK (name, version, etc) for inclusion in envelope headers */
public metadata: SdkMetadata;
public sdkInfo?: SdkInfo;

/** The internally used Dsn object. */
private readonly _dsnObject: Dsn;

/** Create a new instance of API */
public constructor(dsn: DsnLike, metadata: SdkMetadata = {}) {
public constructor(dsn: DsnLike, sdkInfo?: SdkInfo) {
this.dsn = dsn;
this._dsnObject = new Dsn(dsn);
this.metadata = metadata;
this.sdkInfo = sdkInfo;
}

/** Returns the Dsn object. */
Expand Down
11 changes: 10 additions & 1 deletion packages/core/src/baseclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,15 @@ export abstract class BaseClient<B extends Backend, O extends Options> implement
protected constructor(backendClass: BackendClass<B, O>, options: O) {
this._backend = new backendClass(options);
this._options = options;
this._options._internal = this._options._internal || {};

// Both `defaultIntegrations` and `discoverIntegrations` should be `boolean`, but we are stuck with this type
// for backwards compatibility at the momement.
if (options.defaultIntegrations !== false) {
this._options._internal.defaultIntegrations = Array.isArray(this._options.defaultIntegrations)
? this._options.defaultIntegrations
: [];
}

if (options.dsn) {
this._dsn = new Dsn(options.dsn);
Expand Down Expand Up @@ -202,7 +211,7 @@ export abstract class BaseClient<B extends Backend, O extends Options> implement
*/
public setupIntegrations(): void {
if (this._isEnabled()) {
this._integrations = setupIntegrations(this._options);
this._integrations = setupIntegrations(this.getOptions());
}
}

Expand Down
83 changes: 45 additions & 38 deletions packages/core/src/integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,47 +10,50 @@ export interface IntegrationIndex {
}

/** Gets integration to install */
export function getIntegrationsToSetup(options: Options): Integration[] {
const defaultIntegrations = (options.defaultIntegrations && [...options.defaultIntegrations]) || [];
const userIntegrations = options.integrations;
let integrations: Integration[] = [];
if (Array.isArray(userIntegrations)) {
const userIntegrationsNames = userIntegrations.map(i => i.name);
const pickedIntegrationsNames: string[] = [];
export function getIntegrationsToSetup(integrations: {
defaultIntegrations?: Integration[];
discoveredIntegrations?: Integration[];
userIntegrations?: Integration[] | ((integrations: Integration[]) => Integration[]);
}): Integration[] {
const { discoveredIntegrations = [], userIntegrations = [] } = integrations;
let { defaultIntegrations = [] } = integrations;

// Leave only unique default integrations, that were not overridden with provided user integrations
defaultIntegrations.forEach(defaultIntegration => {
if (
userIntegrationsNames.indexOf(defaultIntegration.name) === -1 &&
pickedIntegrationsNames.indexOf(defaultIntegration.name) === -1
) {
integrations.push(defaultIntegration);
pickedIntegrationsNames.push(defaultIntegration.name);
}
});
// And filter out duplicated default integrations
defaultIntegrations = defaultIntegrations.reduce((acc, defaultIntegration) => {
if (acc.every(accIntegration => defaultIntegration.name !== accIntegration.name)) {
acc.push(defaultIntegration);
}
return acc;
}, [] as Integration[]);

// Don't add same user integration twice
userIntegrations.forEach(userIntegration => {
if (pickedIntegrationsNames.indexOf(userIntegration.name) === -1) {
integrations.push(userIntegration);
pickedIntegrationsNames.push(userIntegration.name);
}
});
} else if (typeof userIntegrations === 'function') {
integrations = userIntegrations(defaultIntegrations);
integrations = Array.isArray(integrations) ? integrations : [integrations];
} else {
integrations = [...defaultIntegrations];
}
// Filter out default integrations that are also discovered
let processedIntegrations: Integration[] = [
...defaultIntegrations.filter(defaultIntegration =>
discoveredIntegrations.every(discoveredIntegration => discoveredIntegration.name !== defaultIntegration.name),
),
...discoveredIntegrations,
];

// Make sure that if present, `Debug` integration will always run last
const integrationsNames = integrations.map(i => i.name);
const alwaysLastToRun = 'Debug';
if (integrationsNames.indexOf(alwaysLastToRun) !== -1) {
integrations.push(...integrations.splice(integrationsNames.indexOf(alwaysLastToRun), 1));
if (Array.isArray(userIntegrations)) {
// Filter out integrations that are also included in user integrations
processedIntegrations = [
...processedIntegrations.filter(integrations =>
(userIntegrations as Integration[]).every(userIntegration => userIntegration.name !== integrations.name),
),
// And filter out duplicated user integrations
...userIntegrations.reduce((acc, userIntegration) => {
if (acc.every(accIntegration => userIntegration.name !== accIntegration.name)) {
acc.push(userIntegration);
}
return acc;
}, [] as Integration[]),
];
} else if (typeof userIntegrations === 'function') {
processedIntegrations = userIntegrations(processedIntegrations);
processedIntegrations = Array.isArray(processedIntegrations) ? processedIntegrations : [processedIntegrations];
}

return integrations;
return processedIntegrations;
}

/** Setup given integration */
Expand All @@ -69,9 +72,13 @@ export function setupIntegration(integration: Integration): void {
* @param integrations array of integration instances
* @param withDefault should enable default integrations
*/
export function setupIntegrations<O extends Options>(options: O): IntegrationIndex {
export function setupIntegrations(options: Options): IntegrationIndex {
const integrations: IntegrationIndex = {};
getIntegrationsToSetup(options).forEach(integration => {
getIntegrationsToSetup({
defaultIntegrations: options._internal?.defaultIntegrations || [],
discoveredIntegrations: options._internal?.discoveredIntegrations || [],
userIntegrations: options.integrations || [],
}).forEach(integration => {
integrations[integration.name] = integration;
setupIntegration(integration);
});
Expand Down
22 changes: 8 additions & 14 deletions packages/core/src/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,6 @@ import { Event, SdkInfo, SentryRequest, Session } from '@sentry/types';

import { API } from './api';

/** Extract sdk info from from the API metadata */
function getSdkMetadataForEnvelopeHeader(api: API): SdkInfo | undefined {
if (!api.metadata || !api.metadata.sdk) {
return;
}
const { name, version } = api.metadata.sdk;
return { name, version };
}

/**
* Apply SdkInfo (name, version, packages, integrations) to the corresponding event key.
* Merge with existing data if any.
Expand All @@ -29,10 +20,9 @@ function enhanceEventWithSdkInfo(event: Event, sdkInfo?: SdkInfo): Event {

/** Creates a SentryRequest from a Session. */
export function sessionToSentryRequest(session: Session, api: API): SentryRequest {
const sdkInfo = getSdkMetadataForEnvelopeHeader(api);
const envelopeHeaders = JSON.stringify({
sent_at: new Date().toISOString(),
...(sdkInfo && { sdk: sdkInfo }),
...(api.sdkInfo && { sdk: api.sdkInfo }),
});
const itemHeaders = JSON.stringify({
type: 'session',
Expand All @@ -47,7 +37,6 @@ export function sessionToSentryRequest(session: Session, api: API): SentryReques

/** Creates a SentryRequest from an event. */
export function eventToSentryRequest(event: Event, api: API): SentryRequest {
const sdkInfo = getSdkMetadataForEnvelopeHeader(api);
const eventType = event.type || 'event';
const useEnvelope = eventType === 'transaction';

Expand All @@ -60,7 +49,7 @@ export function eventToSentryRequest(event: Event, api: API): SentryRequest {
}

const req: SentryRequest = {
body: JSON.stringify(sdkInfo ? enhanceEventWithSdkInfo(event, api.metadata.sdk) : event),
body: JSON.stringify(api.sdkInfo ? enhanceEventWithSdkInfo(event, api.sdkInfo) : event),
type: eventType,
url: useEnvelope ? api.getEnvelopeEndpointWithUrlEncodedAuth() : api.getStoreEndpointWithUrlEncodedAuth(),
};
Expand All @@ -75,7 +64,12 @@ export function eventToSentryRequest(event: Event, api: API): SentryRequest {
const envelopeHeaders = JSON.stringify({
event_id: event.event_id,
sent_at: new Date().toISOString(),
...(sdkInfo && { sdk: sdkInfo }),
...(api.sdkInfo && {
sdk: {
name: api.sdkInfo.name,
version: api.sdkInfo.version,
},
}),
});
const itemHeaders = JSON.stringify({
type: event.type,
Expand Down
Loading