Skip to content
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
16 changes: 7 additions & 9 deletions packages/browser/src/backend.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Backend, DSN, Options, SentryError } from '@sentry/core';
import { SentryEvent, SentryResponse } from '@sentry/types';
import { Backend, logger, Options, SentryError } from '@sentry/core';
import { SentryEvent, SentryResponse, Status } from '@sentry/types';
import { isDOMError, isDOMException, isError, isErrorEvent, isPlainObject } from '@sentry/utils/is';
import { supportsFetch } from '@sentry/utils/supports';
import { eventFromStacktrace, getEventOptionsFromPlainObject, prepareFramesForEvent } from './parsers';
Expand Down Expand Up @@ -132,18 +132,16 @@ export class BrowserBackend implements Backend {
* @inheritDoc
*/
public async sendEvent(event: SentryEvent): Promise<SentryResponse> {
let dsn: DSN;

if (!this.options.dsn) {
throw new SentryError('Cannot sendEvent without a valid DSN');
} else {
dsn = new DSN(this.options.dsn);
logger.warn(`Event has been skipped because no DSN is configured.`);
// We do nothing in case there is no DSN
return { status: Status.Skipped };
}

const transportOptions = this.options.transportOptions ? this.options.transportOptions : { dsn };
const transportOptions = this.options.transportOptions ? this.options.transportOptions : { dsn: this.options.dsn };

const transport = this.options.transport
? new this.options.transport({ dsn })
? new this.options.transport({ dsn: this.options.dsn })
: supportsFetch()
? new FetchTransport(transportOptions)
: new XHRTransport(transportOptions);
Expand Down
29 changes: 2 additions & 27 deletions packages/browser/src/client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BaseClient, DSN, SentryError } from '@sentry/core';
import { API, BaseClient, SentryError } from '@sentry/core';
import { DSNLike } from '@sentry/types';
import { getGlobalObject } from '@sentry/utils/misc';
import { BrowserBackend, BrowserOptions } from './backend';
Expand Down Expand Up @@ -57,34 +57,9 @@ export class BrowserClient extends BaseClient<BrowserBackend, BrowserOptions> {
throw new SentryError('Missing `DSN` option in showReportDialog call');
}

const encodedOptions = [];
for (const key in options) {
if (key === 'user') {
const user = options.user;
if (!user) {
continue;
}

if (user.name) {
encodedOptions.push(`name=${encodeURIComponent(user.name)}`);
}
if (user.email) {
encodedOptions.push(`email=${encodeURIComponent(user.email)}`);
}
} else {
encodedOptions.push(`${encodeURIComponent(key)}=${encodeURIComponent(options[key] as string)}`);
}
}

const parsedDSN = new DSN(dsn);
const protocol = parsedDSN.protocol ? `${parsedDSN.protocol}:` : '';
const port = parsedDSN.port ? `:${parsedDSN.port}` : '';
const path = parsedDSN.path ? `/${parsedDSN.path}` : '';
const src = `${protocol}//${parsedDSN.host}${port}${path}/api/embed/error-page/?${encodedOptions.join('&')}`;

const script = document.createElement('script');
script.async = true;
script.src = src;
script.src = new API(dsn).getReportDialogEndpoint(options);
(document.head || document.body).appendChild(script);
}
}
33 changes: 3 additions & 30 deletions packages/browser/src/transports/base.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { DSN, SentryError } from '@sentry/core';
import { DSNComponents, SentryEvent, SentryResponse, Transport, TransportOptions } from '@sentry/types';
import { urlEncode } from '@sentry/utils/object';
import { API, SentryError } from '@sentry/core';
import { SentryEvent, SentryResponse, Transport, TransportOptions } from '@sentry/types';

/** Base Transport class implementation */
export abstract class BaseTransport implements Transport {
Expand All @@ -10,33 +9,7 @@ export abstract class BaseTransport implements Transport {
public url: string;

public constructor(public options: TransportOptions) {
this.url = this.composeUrl(new DSN(options.dsn));
}

/**
* @inheritDoc
*/
public composeUrl(dsn: DSNComponents): string {
const auth = {
sentry_key: dsn.user,
sentry_secret: '',
sentry_version: '7',
};

if (dsn.pass) {
auth.sentry_secret = dsn.pass;
} else {
delete auth.sentry_secret;
}

const protocol = dsn.protocol ? `${dsn.protocol}:` : '';
const port = dsn.port ? `:${dsn.port}` : '';
const path = dsn.path ? `/${dsn.path}` : '';
const endpoint = `${protocol}//${dsn.host}${port}${path}/api/${dsn.projectId}/store/`;

// Auth is intentionally sent as part of query string (NOT as custom HTTP header)
// to avoid preflight CORS requests
return `${endpoint}?${urlEncode(auth)}`;
this.url = new API(this.options.dsn).getStoreEndpointWithUrlEncodedAuth();
}

/**
Expand Down
14 changes: 1 addition & 13 deletions packages/browser/test/transports/base.test.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,9 @@
import { DSNComponents } from '@sentry/types';
import { expect } from 'chai';
import { BaseTransport } from '../../src/transports/base';

const testDSN = 'https://123@sentry.io/42';

class SimpleTransport extends BaseTransport {}
// tslint:disable-next-line:max-classes-per-file
class ComplexTransport extends BaseTransport {
public composeUrl(dsn: DSNComponents): string {
return `https://${dsn.host}/${dsn.user}`;
}
}

describe('BaseTransport', () => {
it('doesnt provide send() implementation', async () => {
Expand All @@ -23,13 +16,8 @@ describe('BaseTransport', () => {
}
});

it('provides composeEndpointUrl() implementation', () => {
it('has correct endpoint url', () => {
const transport = new SimpleTransport({ dsn: testDSN });
expect(transport.url).equal('https://sentry.io/api/42/store/?sentry_key=123&sentry_version=7');
});

it('allows overriding composeEndpointUrl() implementation', () => {
const transport = new ComplexTransport({ dsn: testDSN });
expect(transport.url).equal('https://sentry.io/123');
});
});
97 changes: 97 additions & 0 deletions packages/core/src/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { DSNLike } from '@sentry/types';
import { urlEncode } from '@sentry/utils/object';
import { DSN } from './dsn';

const SENTRY_API_VERSION = '7';

/** Helper class to provide urls to different Sentry endpoints. */
export class API {
/** The internally used DSN object. */
private readonly dsnObject: DSN;
/** Create a new instance of API */
public constructor(public dsn: DSNLike) {
this.dsnObject = new DSN(dsn);
}

/** Returns the DSN object. */
public getDSN(): DSN {
return this.dsnObject;
}

/** Returns a string with auth headers in the url to the store endpoint. */
public getStoreEndpoint(): string {
return `${this.getBaseUrl()}${this.getStoreEndpointPath()}`;
}

/** Returns the store endpoint with auth added in url encoded. */
public getStoreEndpointWithUrlEncodedAuth(): string {
const dsn = this.dsnObject;
const auth = {
sentry_key: dsn.user,
sentry_version: SENTRY_API_VERSION,
};
// Auth is intentionally sent as part of query string (NOT as custom HTTP header)
// to avoid preflight CORS requests
return `${this.getStoreEndpoint()}?${urlEncode(auth)}`;
}

/** Returns the base path of the url including the port. */
private getBaseUrl(): string {
const dsn = this.dsnObject;
const protocol = dsn.protocol ? `${dsn.protocol}:` : '';
const port = dsn.port ? `:${dsn.port}` : '';
return `${protocol}//${dsn.host}${port}`;
}

/** Returns only the path component for the store endpoint. */
public getStoreEndpointPath(): string {
const dsn = this.dsnObject;
return `${dsn.path ? `/${dsn.path}` : ''}/api/${dsn.projectId}/store/`;
}

/** Returns an object that can be used in request headers. */
public getRequestHeaders(clientName: string, clientVersion: string): { [key: string]: string } {
const dsn = this.dsnObject;
const header = [`Sentry sentry_version=${SENTRY_API_VERSION}`];
header.push(`sentry_timestamp=${new Date().getTime()}`);
header.push(`sentry_client=${clientName}/${clientVersion}`);
Copy link
Contributor

Choose a reason for hiding this comment

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

here we use sentry_client and above we used sentry_version. We should unify it somehow.

Copy link
Member Author

Choose a reason for hiding this comment

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

not really sure what you mean, we need sentry_client for sending a POST.

header.push(`sentry_key=${dsn.user}`);
return {
'Content-Type': 'application/json',
'X-Sentry-Auth': header.join(', '),
};
}

/** Returns the url to the report dialog endpoint. */
public getReportDialogEndpoint(
dialogOptions: {
[key: string]: any;
user?: { name?: string; email?: string };
} = {},
): string {
const dsn = this.dsnObject;
const endpoint = `${this.getBaseUrl()}${dsn.path ? `/${dsn.path}` : ''}/api/embed/error-page/`;

const encodedOptions = [];
for (const key in dialogOptions) {
if (key === 'user') {
if (!dialogOptions.user) {
continue;
}
if (dialogOptions.user.name) {
encodedOptions.push(`name=${encodeURIComponent(dialogOptions.user.name)}`);
}
if (dialogOptions.user.email) {
encodedOptions.push(`email=${encodeURIComponent(dialogOptions.user.email)}`);
}
} else {
encodedOptions.push(`${encodeURIComponent(key)}=${encodeURIComponent(dialogOptions[key] as string)}`);
}
}
if (encodedOptions.length) {
return `${endpoint}?${encodedOptions.join('&')}`;
}

return endpoint;
}
}
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export { logger } from './logger';
export { captureException, captureMessage, configureScope } from '@sentry/minimal';
export { Hub, Scope } from '@sentry/hub';
export { API } from './api';
export { BackendClass, BaseClient } from './base';
export { DSN } from './dsn';
export { SentryError } from './error';
Expand Down
53 changes: 53 additions & 0 deletions packages/core/test/lib/api.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { API } from '../../src/api';
import { DSN } from '../../src/dsn';

const dsnPublic = 'https://abc@sentry.io:1234/subpath/123';

describe('API', () => {
test('getStoreEndpoint', () => {
expect(new API(dsnPublic).getStoreEndpointWithUrlEncodedAuth()).toEqual(
'https://sentry.io:1234/subpath/api/123/store/?sentry_key=abc&sentry_version=7',
);
expect(new API(dsnPublic).getStoreEndpoint()).toEqual('https://sentry.io:1234/subpath/api/123/store/');
});

test('getRequestHeaders', () => {
expect(new API(dsnPublic).getRequestHeaders('a', '1.0')).toMatchObject({
'Content-Type': 'application/json',
'X-Sentry-Auth': expect.stringMatching(
/^Sentry sentry_version=\d, sentry_timestamp=\d+, sentry_client=a\/1\.0, sentry_key=abc$/,
),
});
});
test('getReportDialogEndpoint', () => {
expect(new API(dsnPublic).getReportDialogEndpoint({})).toEqual(
'https://sentry.io:1234/subpath/api/embed/error-page/',
);
expect(
new API(dsnPublic).getReportDialogEndpoint({
eventId: 'abc',
testy: '2',
}),
).toEqual('https://sentry.io:1234/subpath/api/embed/error-page/?eventId=abc&testy=2');

expect(
new API(dsnPublic).getReportDialogEndpoint({
eventId: 'abc',
user: {
email: 'email',
name: 'yo',
},
}),
).toEqual('https://sentry.io:1234/subpath/api/embed/error-page/?eventId=abc&name=yo&email=email');

expect(
new API(dsnPublic).getReportDialogEndpoint({
eventId: 'abc',
user: undefined,
}),
).toEqual('https://sentry.io:1234/subpath/api/embed/error-page/?eventId=abc');
});
test('getDSN', () => {
expect(new API(dsnPublic).getDSN()).toEqual(new DSN(dsnPublic));
});
});
31 changes: 8 additions & 23 deletions packages/node/src/transports/base.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DSN, SentryError } from '@sentry/core';
import { API, SentryError } from '@sentry/core';
import { SentryEvent, SentryResponse, Status, Transport, TransportOptions } from '@sentry/types';
import { serialize } from '@sentry/utils/object';
import * as http from 'http';
Expand All @@ -15,46 +15,31 @@ export interface HTTPRequest {

/** Base Transport class implementation */
export abstract class BaseTransport implements Transport {
/** DSN object */
protected dsn: DSN;
/** API object */
protected api: API;

/** The Agent used for corresponding transport */
protected client: http.Agent | https.Agent | undefined;

/** Create instance and set this.dsn */
public constructor(public options: TransportOptions) {
this.dsn = new DSN(options.dsn);
}

/** Returns a Sentry auth header string */
private getAuthHeader(): string {
const header = ['Sentry sentry_version=7'];
header.push(`sentry_timestamp=${new Date().getTime()}`);

header.push(`sentry_client=${SDK_NAME}/${SDK_VERSION}`);

header.push(`sentry_key=${this.dsn.user}`);
if (this.dsn.pass) {
header.push(`sentry_secret=${this.dsn.pass}`);
}
return header.join(', ');
this.api = new API(options.dsn);
}

/** Returns a build request option object used by request */
protected getRequestOptions(): http.RequestOptions {
const headers = {
'Content-Type': 'application/json',
'X-Sentry-Auth': this.getAuthHeader(),
...this.api.getRequestHeaders(SDK_NAME, SDK_VERSION),
...this.options.headers,
};

return {
agent: this.client,
headers,
hostname: this.dsn.host,
hostname: this.api.getDSN().host,
method: 'POST',
path: `${this.dsn.path ? `/${this.dsn.path}` : ''}/api/${this.dsn.projectId}/store/`,
port: this.dsn.port,
path: this.api.getStoreEndpointPath(),
port: this.api.getDSN().port,
};
}

Expand Down