-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
feat(nextjs): Add Edge Runtime SDK #6752
Merged
Merged
Changes from 3 commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import type { Scope } from '@sentry/core'; | ||
import { BaseClient, SDK_VERSION } from '@sentry/core'; | ||
import type { ClientOptions, Event, EventHint, Severity, SeverityLevel } from '@sentry/types'; | ||
|
||
import { eventFromMessage, eventFromUnknownInput } from './eventbuilder'; | ||
import type { EdgeTransportOptions } from './transport'; | ||
|
||
export type EdgeClientOptions = ClientOptions<EdgeTransportOptions>; | ||
|
||
/** | ||
* The Sentry Edge SDK Client. | ||
*/ | ||
export class EdgeClient extends BaseClient<EdgeClientOptions> { | ||
/** | ||
* Creates a new Edge SDK instance. | ||
* @param options Configuration options for this SDK. | ||
*/ | ||
public constructor(options: EdgeClientOptions) { | ||
options._metadata = options._metadata || {}; | ||
options._metadata.sdk = options._metadata.sdk || { | ||
name: 'sentry.javascript.nextjs', | ||
packages: [ | ||
{ | ||
name: 'npm:@sentry/nextjs', | ||
version: SDK_VERSION, | ||
}, | ||
], | ||
version: SDK_VERSION, | ||
}; | ||
|
||
super(options); | ||
} | ||
|
||
/** | ||
* @inheritDoc | ||
*/ | ||
public eventFromException(exception: unknown, hint?: EventHint): PromiseLike<Event> { | ||
return Promise.resolve(eventFromUnknownInput(this._options.stackParser, exception, hint)); | ||
} | ||
|
||
/** | ||
* @inheritDoc | ||
*/ | ||
public eventFromMessage( | ||
message: string, | ||
// eslint-disable-next-line deprecation/deprecation | ||
level: Severity | SeverityLevel = 'info', | ||
hint?: EventHint, | ||
): PromiseLike<Event> { | ||
return Promise.resolve( | ||
eventFromMessage(this._options.stackParser, message, level, hint, this._options.attachStacktrace), | ||
); | ||
} | ||
|
||
/** | ||
* @inheritDoc | ||
*/ | ||
protected _prepareEvent(event: Event, hint: EventHint, scope?: Scope): PromiseLike<Event | null> { | ||
event.platform = event.platform || 'edge'; | ||
event.contexts = { | ||
...event.contexts, | ||
runtime: event.contexts?.runtime || { | ||
name: 'edge', | ||
}, | ||
}; | ||
event.server_name = event.server_name || process.env.SENTRY_NAME; | ||
return super._prepareEvent(event, hint, scope); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
import { getCurrentHub } from '@sentry/core'; | ||
import type { | ||
Event, | ||
EventHint, | ||
Exception, | ||
Mechanism, | ||
Severity, | ||
SeverityLevel, | ||
StackFrame, | ||
StackParser, | ||
} from '@sentry/types'; | ||
import { | ||
addExceptionMechanism, | ||
addExceptionTypeValue, | ||
extractExceptionKeysForMessage, | ||
isError, | ||
isPlainObject, | ||
normalizeToSize, | ||
} from '@sentry/utils'; | ||
|
||
/** | ||
* Extracts stack frames from the error.stack string | ||
*/ | ||
export function parseStackFrames(stackParser: StackParser, error: Error): StackFrame[] { | ||
return stackParser(error.stack || '', 1); | ||
} | ||
|
||
/** | ||
* Extracts stack frames from the error and builds a Sentry Exception | ||
*/ | ||
export function exceptionFromError(stackParser: StackParser, error: Error): Exception { | ||
const exception: Exception = { | ||
type: error.name || error.constructor.name, | ||
value: error.message, | ||
}; | ||
|
||
const frames = parseStackFrames(stackParser, error); | ||
if (frames.length) { | ||
exception.stacktrace = { frames }; | ||
} | ||
|
||
return exception; | ||
} | ||
|
||
/** | ||
* Builds and Event from a Exception | ||
* @hidden | ||
*/ | ||
export function eventFromUnknownInput(stackParser: StackParser, exception: unknown, hint?: EventHint): Event { | ||
let ex: unknown = exception; | ||
const providedMechanism: Mechanism | undefined = | ||
hint && hint.data && (hint.data as { mechanism: Mechanism }).mechanism; | ||
const mechanism: Mechanism = providedMechanism || { | ||
handled: true, | ||
type: 'generic', | ||
}; | ||
|
||
if (!isError(exception)) { | ||
if (isPlainObject(exception)) { | ||
// This will allow us to group events based on top-level keys | ||
// which is much better than creating new group when any key/value change | ||
const message = `Non-Error exception captured with keys: ${extractExceptionKeysForMessage(exception)}`; | ||
|
||
const hub = getCurrentHub(); | ||
const client = hub.getClient(); | ||
const normalizeDepth = client && client.getOptions().normalizeDepth; | ||
hub.configureScope(scope => { | ||
scope.setExtra('__serialized__', normalizeToSize(exception, normalizeDepth)); | ||
}); | ||
|
||
ex = (hint && hint.syntheticException) || new Error(message); | ||
(ex as Error).message = message; | ||
} else { | ||
// This handles when someone does: `throw "something awesome";` | ||
// We use synthesized Error here so we can extract a (rough) stack trace. | ||
ex = (hint && hint.syntheticException) || new Error(exception as string); | ||
(ex as Error).message = exception as string; | ||
} | ||
mechanism.synthetic = true; | ||
} | ||
|
||
const event = { | ||
exception: { | ||
values: [exceptionFromError(stackParser, ex as Error)], | ||
}, | ||
}; | ||
|
||
addExceptionTypeValue(event, undefined, undefined); | ||
addExceptionMechanism(event, mechanism); | ||
|
||
return { | ||
...event, | ||
event_id: hint && hint.event_id, | ||
}; | ||
} | ||
|
||
/** | ||
* Builds and Event from a Message | ||
* @hidden | ||
*/ | ||
export function eventFromMessage( | ||
stackParser: StackParser, | ||
message: string, | ||
// eslint-disable-next-line deprecation/deprecation | ||
level: Severity | SeverityLevel = 'info', | ||
hint?: EventHint, | ||
attachStacktrace?: boolean, | ||
): Event { | ||
const event: Event = { | ||
event_id: hint && hint.event_id, | ||
level, | ||
message, | ||
}; | ||
|
||
if (attachStacktrace && hint && hint.syntheticException) { | ||
const frames = parseStackFrames(stackParser, hint.syntheticException); | ||
if (frames.length) { | ||
event.exception = { | ||
values: [ | ||
{ | ||
value: message, | ||
stacktrace: { frames }, | ||
}, | ||
], | ||
}; | ||
} | ||
} | ||
|
||
return event; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
import '@sentry/tracing'; // Allow people to call tracing API methods without explicitly importing the tracing package. | ||
|
||
import { getCurrentHub, getIntegrationsToSetup, initAndBind, Integrations as CoreIntegrations } from '@sentry/core'; | ||
import type { Options } from '@sentry/types'; | ||
import { | ||
createStackParser, | ||
GLOBAL_OBJ, | ||
logger, | ||
nodeStackLineParser, | ||
stackParserFromStackParserOptions, | ||
} from '@sentry/utils'; | ||
|
||
import { EdgeClient } from './edgeclient'; | ||
import { makeEdgeTransport } from './transport'; | ||
|
||
const nodeStackParser = createStackParser(nodeStackLineParser()); | ||
|
||
export const defaultIntegrations = [new CoreIntegrations.InboundFilters(), new CoreIntegrations.FunctionToString()]; | ||
|
||
export type EdgeOptions = Options; | ||
|
||
/** Inits the Sentry NextJS SDK on the Edge Runtime. */ | ||
export function init(options: EdgeOptions = {}): void { | ||
if (options.defaultIntegrations === undefined) { | ||
options.defaultIntegrations = defaultIntegrations; | ||
} | ||
|
||
if (options.dsn === undefined && process.env.SENTRY_DSN) { | ||
options.dsn = process.env.SENTRY_DSN; | ||
} | ||
|
||
if (options.tracesSampleRate === undefined && process.env.SENTRY_TRACES_SAMPLE_RATE) { | ||
const tracesSampleRate = parseFloat(process.env.SENTRY_TRACES_SAMPLE_RATE); | ||
if (isFinite(tracesSampleRate)) { | ||
options.tracesSampleRate = tracesSampleRate; | ||
} | ||
} | ||
|
||
if (options.release === undefined) { | ||
const detectedRelease = getSentryRelease(); | ||
if (detectedRelease !== undefined) { | ||
options.release = detectedRelease; | ||
} else { | ||
// If release is not provided, then we should disable autoSessionTracking | ||
options.autoSessionTracking = false; | ||
} | ||
} | ||
|
||
if (options.environment === undefined && process.env.SENTRY_ENVIRONMENT) { | ||
options.environment = process.env.SENTRY_ENVIRONMENT; | ||
} | ||
|
||
if (options.autoSessionTracking === undefined && options.dsn !== undefined) { | ||
options.autoSessionTracking = true; | ||
} | ||
|
||
if (options.instrumenter === undefined) { | ||
options.instrumenter = 'sentry'; | ||
} | ||
|
||
const clientOptions = { | ||
...options, | ||
stackParser: stackParserFromStackParserOptions(options.stackParser || nodeStackParser), | ||
integrations: getIntegrationsToSetup(options), | ||
transport: options.transport || makeEdgeTransport, | ||
}; | ||
|
||
initAndBind(EdgeClient, clientOptions); | ||
|
||
// TODO?: Sessiontracking | ||
} | ||
|
||
/** | ||
* Returns a release dynamically from environment variables. | ||
*/ | ||
export function getSentryRelease(fallback?: string): string | undefined { | ||
// Always read first as Sentry takes this as precedence | ||
if (process.env.SENTRY_RELEASE) { | ||
return process.env.SENTRY_RELEASE; | ||
} | ||
|
||
// This supports the variable that sentry-webpack-plugin injects | ||
if (GLOBAL_OBJ.SENTRY_RELEASE && GLOBAL_OBJ.SENTRY_RELEASE.id) { | ||
return GLOBAL_OBJ.SENTRY_RELEASE.id; | ||
} | ||
|
||
return ( | ||
// GitHub Actions - https://help.github.com/en/actions/configuring-and-managing-workflows/using-environment-variables#default-environment-variables | ||
process.env.GITHUB_SHA || | ||
// Netlify - https://docs.netlify.com/configure-builds/environment-variables/#build-metadata | ||
process.env.COMMIT_REF || | ||
// Vercel - https://vercel.com/docs/v2/build-step#system-environment-variables | ||
process.env.VERCEL_GIT_COMMIT_SHA || | ||
process.env.VERCEL_GITHUB_COMMIT_SHA || | ||
process.env.VERCEL_GITLAB_COMMIT_SHA || | ||
process.env.VERCEL_BITBUCKET_COMMIT_SHA || | ||
// Zeit (now known as Vercel) | ||
process.env.ZEIT_GITHUB_COMMIT_SHA || | ||
process.env.ZEIT_GITLAB_COMMIT_SHA || | ||
process.env.ZEIT_BITBUCKET_COMMIT_SHA || | ||
fallback | ||
); | ||
} | ||
|
||
/** | ||
* Call `close()` on the current client, if there is one. See {@link Client.close}. | ||
* | ||
* @param timeout Maximum time in ms the client should wait to flush its event queue before shutting down. Omitting this | ||
* parameter will cause the client to wait until all events are sent before disabling itself. | ||
* @returns A promise which resolves to `true` if the queue successfully drains before the timeout, or `false` if it | ||
* doesn't (or if there's no client defined). | ||
*/ | ||
export async function close(timeout?: number): Promise<boolean> { | ||
const client = getCurrentHub().getClient<EdgeClient>(); | ||
if (client) { | ||
return client.close(timeout); | ||
} | ||
__DEBUG_BUILD__ && logger.warn('Cannot flush events and disable SDK. No client defined.'); | ||
return Promise.resolve(false); | ||
} | ||
|
||
/** | ||
* Call `flush()` on the current client, if there is one. See {@link Client.flush}. | ||
* | ||
* @param timeout Maximum time in ms the client should wait to flush its event queue. Omitting this parameter will cause | ||
* the client to wait until all events are sent before resolving the promise. | ||
* @returns A promise which resolves to `true` if the queue successfully drains before the timeout, or `false` if it | ||
* doesn't (or if there's no client defined). | ||
*/ | ||
export async function flush(timeout?: number): Promise<boolean> { | ||
const client = getCurrentHub().getClient<EdgeClient>(); | ||
if (client) { | ||
return client.flush(timeout); | ||
} | ||
__DEBUG_BUILD__ && logger.warn('Cannot flush events. No client defined.'); | ||
return Promise.resolve(false); | ||
} | ||
|
||
/** | ||
* This is the getter for lastEventId. | ||
* | ||
* @returns The last event id of a captured event. | ||
*/ | ||
export function lastEventId(): string | undefined { | ||
return getCurrentHub().lastEventId(); | ||
} | ||
|
||
export * from '@sentry/core'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import { createTransport } from '@sentry/core'; | ||
import type { BaseTransportOptions, Transport, TransportMakeRequestResponse, TransportRequest } from '@sentry/types'; | ||
|
||
export interface EdgeTransportOptions extends BaseTransportOptions { | ||
/** Fetch API init parameters. Used by the FetchTransport */ | ||
fetchOptions?: RequestInit; | ||
/** Custom headers for the transport. Used by the XHRTransport and FetchTransport */ | ||
headers?: { [key: string]: string }; | ||
} | ||
|
||
/** | ||
* Creates a Transport that uses the Edge Runtimes native fetch API to send events to Sentry. | ||
*/ | ||
export function makeEdgeTransport(options: EdgeTransportOptions): Transport { | ||
function makeRequest(request: TransportRequest): PromiseLike<TransportMakeRequestResponse> { | ||
const requestOptions: RequestInit = { | ||
body: request.body, | ||
method: 'POST', | ||
referrerPolicy: 'origin', | ||
headers: options.headers, | ||
...options.fetchOptions, | ||
}; | ||
|
||
try { | ||
return fetch(options.url, requestOptions).then(response => ({ | ||
statusCode: response.status, | ||
headers: { | ||
'x-sentry-rate-limits': response.headers.get('X-Sentry-Rate-Limits'), | ||
'retry-after': response.headers.get('Retry-After'), | ||
}, | ||
})); | ||
} catch (e) { | ||
return Promise.reject(e); | ||
} | ||
} | ||
|
||
return createTransport(options, makeRequest); | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the future we can include
RequestData
andLinkedErrors
here since they are agnostic to node, but that requires some more refactoring.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LinkedErrors could probably even live in
@sentry/core
. We currently duplicate it in browser & node. As forRequestData
I am a bit unsure. That integration has become a bit of a behemoth and I don't like it becoming even more generic.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is slightly different behaviour for node/browser 😅
RequestData
being overloaded is fair, we can probably re-examine this.