diff --git a/.craft.yml b/.craft.yml index 94e5b37d02a0..ba9b1d13322a 100644 --- a/.craft.yml +++ b/.craft.yml @@ -1,6 +1,11 @@ minVersion: '0.23.1' changelogPolicy: simple preReleaseCommand: bash scripts/craft-pre-release.sh +statusProvider: + name: github + config: + contexts: + - job_required_jobs_passed targets: # NPM Targets ## 1. Base Packages, node or browser SDKs depend on diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 696ae9310d11..4cfa56ec640e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -599,7 +599,7 @@ jobs: needs: [job_get_metadata, job_build] if: needs.job_get_metadata.outputs.changed_browser == 'true' || github.event_name != 'pull_request' runs-on: ubuntu-20.04 - timeout-minutes: 10 + timeout-minutes: 20 strategy: fail-fast: false matrix: @@ -855,8 +855,8 @@ jobs: timeout-minutes: 5 run: yarn test:assert - job_required_tests: - name: All required tests passed or skipped + job_required_jobs_passed: + name: All required jobs passed or were skipped needs: [ job_build, @@ -870,6 +870,9 @@ jobs: job_browser_loader_tests, job_remix_integration_tests, job_e2e_tests, + job_artifacts, + job_lint, + job_circular_dep_check, ] # Always run this, even if a dependent job failed if: always() diff --git a/CHANGELOG.md b/CHANGELOG.md index 89b0e70865e5..eb8cc846b983 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,42 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 7.62.0 + +### Important Changes + +- **feat(integrations): Add `ContextLines` integration for html-embedded JS stack frames (#8699)** + +This release adds the `ContextLines` integration as an optional integration for the Browser SDKs to `@sentry/integrations`. + +This integration adds source code from inline JavaScript of the current page's HTML (e.g. JS in ` + + + + + ', + '', + ], + }, + { + lineno: 7, + pre_context: [ + '', + '', + '', + ' ', + ' ', + ' ', + ' ', + ' ', + expect.stringContaining('', + ], + }, + ], + }, + }); + }, +); diff --git a/packages/browser/src/exports.ts b/packages/browser/src/exports.ts index f48b00c8c8e8..1f7117620c93 100644 --- a/packages/browser/src/exports.ts +++ b/packages/browser/src/exports.ts @@ -26,11 +26,14 @@ export { captureException, captureEvent, captureMessage, + close, configureScope, createTransport, + flush, getHubFromCarrier, getCurrentHub, Hub, + lastEventId, makeMain, Scope, startTransaction, @@ -60,16 +63,5 @@ export { } from './stack-parsers'; export { eventFromException, eventFromMessage } from './eventbuilder'; export { createUserFeedbackEnvelope } from './userfeedback'; -export { - defaultIntegrations, - forceLoad, - init, - lastEventId, - onLoad, - showReportDialog, - flush, - close, - wrap, - captureUserFeedback, -} from './sdk'; +export { defaultIntegrations, forceLoad, init, onLoad, showReportDialog, wrap, captureUserFeedback } from './sdk'; export { GlobalHandlers, TryCatch, Breadcrumbs, LinkedErrors, HttpContext, Dedupe } from './integrations'; diff --git a/packages/browser/src/sdk.ts b/packages/browser/src/sdk.ts index 17fbfc159549..45fd26c75745 100644 --- a/packages/browser/src/sdk.ts +++ b/packages/browser/src/sdk.ts @@ -7,13 +7,7 @@ import { Integrations as CoreIntegrations, } from '@sentry/core'; import type { UserFeedback } from '@sentry/types'; -import { - addInstrumentationHandler, - logger, - resolvedSyncPromise, - stackParserFromStackParserOptions, - supportsFetch, -} from '@sentry/utils'; +import { addInstrumentationHandler, logger, stackParserFromStackParserOptions, supportsFetch } from '@sentry/utils'; import type { BrowserClientOptions, BrowserOptions } from './client'; import { BrowserClient } from './client'; @@ -179,15 +173,6 @@ export function showReportDialog(options: ReportDialogOptions = {}, hub: Hub = g } } -/** - * This is the getter for lastEventId. - * - * @returns The last event id of a captured event. - */ -export function lastEventId(): string | undefined { - return getCurrentHub().lastEventId(); -} - /** * This function is here to be API compatible with the loader. * @hidden @@ -204,40 +189,6 @@ export function onLoad(callback: () => void): void { callback(); } -/** - * 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 function flush(timeout?: number): PromiseLike { - const client = getCurrentHub().getClient(); - if (client) { - return client.flush(timeout); - } - __DEBUG_BUILD__ && logger.warn('Cannot flush events. No client defined.'); - return resolvedSyncPromise(false); -} - -/** - * 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 function close(timeout?: number): PromiseLike { - const client = getCurrentHub().getClient(); - if (client) { - return client.close(timeout); - } - __DEBUG_BUILD__ && logger.warn('Cannot flush events and disable SDK. No client defined.'); - return resolvedSyncPromise(false); -} - /** * Wrap code within a try/catch block so the SDK is able to capture errors. * diff --git a/packages/core/src/exports.ts b/packages/core/src/exports.ts index ad3d33013253..1a143a2efd4e 100644 --- a/packages/core/src/exports.ts +++ b/packages/core/src/exports.ts @@ -209,3 +209,46 @@ export function captureCheckIn(checkIn: CheckIn, upsertMonitorConfig?: MonitorCo return uuid4(); } + +/** + * 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 { + const client = getCurrentHub().getClient(); + if (client) { + return client.flush(timeout); + } + __DEBUG_BUILD__ && logger.warn('Cannot flush events. No client defined.'); + return Promise.resolve(false); +} + +/** + * 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 { + const client = getCurrentHub().getClient(); + if (client) { + return client.close(timeout); + } + __DEBUG_BUILD__ && logger.warn('Cannot flush events and disable SDK. 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(); +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 9ff61f05cdb3..a0cc7e627bf7 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -5,10 +5,14 @@ export type { OfflineStore, OfflineTransportOptions } from './transports/offline export * from './tracing'; export { addBreadcrumb, + captureCheckIn, captureException, captureEvent, captureMessage, + close, configureScope, + flush, + lastEventId, startTransaction, setContext, setExtra, @@ -17,7 +21,6 @@ export { setTags, setUser, withScope, - captureCheckIn, } from './exports'; export { getCurrentHub, diff --git a/packages/ember/.eslintrc.js b/packages/ember/.eslintrc.js index 9a53dffde69e..83d1d1583fdb 100644 --- a/packages/ember/.eslintrc.js +++ b/packages/ember/.eslintrc.js @@ -1,34 +1,38 @@ 'use strict'; module.exports = { - root: true, - parser: 'babel-eslint', - parserOptions: { - ecmaVersion: 2018, - sourceType: 'module', - ecmaFeatures: { - legacyDecorators: true, - }, - }, - plugins: ['ember'], - extends: ['eslint:recommended', 'plugin:ember/recommended'], - env: { - browser: true, - }, - globals: { - QUnit: true, - }, - rules: {}, + extends: ['../../.eslintrc.js'], + overrides: [ { - files: ['addon/**'], - plugins: ['@sentry-internal/eslint-plugin-sdk'], + // addon files + files: ['{addon,app,tests}/**/*.{js,ts,d.ts}'], + parserOptions: { + sourceType: 'module', + babelOptions: { + plugins: [['@babel/plugin-proposal-decorators', { decoratorsBeforeExport: true }]], + }, + }, + plugins: ['ember'], + extends: ['plugin:ember/recommended'], + rules: { + 'import/no-unresolved': 'off', + }, + }, + { + // test files + files: ['tests/**/*-test.{js,ts}', 'tests/helpers/**/*.{js,ts}'], + extends: ['plugin:qunit/recommended'], + /* globals: { + QUnit: true, + }, */ + rules: { + 'qunit/require-expect': 'off', + }, }, - // node files { files: [ './.eslintrc.js', - './.prettierrc.js', './.template-lintrc.js', './ember-cli-build.js', './index.js', @@ -44,13 +48,7 @@ module.exports = { browser: false, node: true, }, - plugins: ['node'], - extends: ['plugin:node/recommended'], - }, - { - // test files - files: ['tests/**/*-test.{js,ts}'], - extends: ['plugin:qunit/recommended'], + extends: ['plugin:n/recommended'], }, ], }; diff --git a/packages/ember/addon/index.ts b/packages/ember/addon/index.ts index 2db7ac4192f6..fbcea14d5533 100644 --- a/packages/ember/addon/index.ts +++ b/packages/ember/addon/index.ts @@ -1,19 +1,23 @@ -import * as Sentry from '@sentry/browser'; -import { SDK_VERSION, BrowserOptions } from '@sentry/browser'; -import { macroCondition, isDevelopingApp, getOwnConfig } from '@embroider/macros'; -import { next } from '@ember/runloop'; import { assert, warn } from '@ember/debug'; +import type Route from '@ember/routing/route'; +import { next } from '@ember/runloop'; +import { getOwnConfig, isDevelopingApp, macroCondition } from '@embroider/macros'; +import type { BrowserOptions } from '@sentry/browser'; +import * as Sentry from '@sentry/browser'; +import { SDK_VERSION } from '@sentry/browser'; +import type { Transaction } from '@sentry/types'; +import { GLOBAL_OBJ, timestampInSeconds } from '@sentry/utils'; import Ember from 'ember'; -import { timestampInSeconds, GLOBAL_OBJ } from '@sentry/utils'; -import { GlobalConfig, OwnConfig } from './types'; -function _getSentryInitConfig() { +import type { EmberSentryConfig, GlobalConfig, OwnConfig } from './types'; + +function _getSentryInitConfig(): EmberSentryConfig['sentry'] { const _global = GLOBAL_OBJ as typeof GLOBAL_OBJ & GlobalConfig; _global.__sentryEmberConfig = _global.__sentryEmberConfig ?? {}; return _global.__sentryEmberConfig; } -export function InitSentryForEmber(_runtimeConfig?: BrowserOptions) { +export function InitSentryForEmber(_runtimeConfig?: BrowserOptions): void { const environmentConfig = getOwnConfig().sentryConfig; assert('Missing configuration.', environmentConfig); @@ -21,7 +25,7 @@ export function InitSentryForEmber(_runtimeConfig?: BrowserOptions) { if (!environmentConfig.sentry) { // If environment config is not specified but the above assertion passes, use runtime config. - environmentConfig.sentry = { ..._runtimeConfig } as any; + environmentConfig.sentry = { ..._runtimeConfig }; } // Merge runtime config into environment config, preferring runtime. @@ -62,12 +66,20 @@ export function InitSentryForEmber(_runtimeConfig?: BrowserOptions) { } } -export const getActiveTransaction = () => { +export const getActiveTransaction = (): Transaction | undefined => { return Sentry.getCurrentHub().getScope().getTransaction(); }; -export const instrumentRoutePerformance = (BaseRoute: any) => { - const instrumentFunction = async (op: string, description: string, fn: Function, args: any) => { +type RouteConstructor = new (...args: ConstructorParameters) => Route; + +export const instrumentRoutePerformance = (BaseRoute: T): T => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const instrumentFunction = async any>( + op: string, + description: string, + fn: X, + args: Parameters, + ): Promise> => { const startTimestamp = timestampInSeconds(); const result = await fn(...args); @@ -79,40 +91,38 @@ export const instrumentRoutePerformance = (BaseRoute: any) => { return result; }; + const routeName = BaseRoute.name; + return { - [BaseRoute.name]: class extends BaseRoute { - beforeModel(...args: any[]) { + // @ts-expect-error TS2545 We do not need to redefine a constructor here + [routeName]: class extends BaseRoute { + public beforeModel(...args: unknown[]): void | Promise { return instrumentFunction( 'ui.ember.route.before_model', - (this).fullRouteName, + this.fullRouteName, super.beforeModel.bind(this), args, ); } - async model(...args: any[]) { - return instrumentFunction('ui.ember.route.model', (this).fullRouteName, super.model.bind(this), args); + public async model(...args: unknown[]): Promise { + return instrumentFunction('ui.ember.route.model', this.fullRouteName, super.model.bind(this), args); } - async afterModel(...args: any[]) { - return instrumentFunction( - 'ui.ember.route.after_model', - (this).fullRouteName, - super.afterModel.bind(this), - args, - ); + public afterModel(...args: unknown[]): void | Promise { + return instrumentFunction('ui.ember.route.after_model', this.fullRouteName, super.afterModel.bind(this), args); } - async setupController(...args: any[]) { + public setupController(...args: unknown[]): void | Promise { return instrumentFunction( 'ui.ember.route.setup_controller', - (this).fullRouteName, + this.fullRouteName, super.setupController.bind(this), args, ); } }, - }[BaseRoute.name]; + }[routeName] as T; }; export * from '@sentry/browser'; diff --git a/packages/ember/addon/instance-initializers/sentry-performance.ts b/packages/ember/addon/instance-initializers/sentry-performance.ts index 1e575f71fe76..2b962f4df2ae 100644 --- a/packages/ember/addon/instance-initializers/sentry-performance.ts +++ b/packages/ember/addon/instance-initializers/sentry-performance.ts @@ -1,18 +1,26 @@ -import ApplicationInstance from '@ember/application/instance'; -import { run, _backburner, scheduleOnce } from '@ember/runloop'; +/* eslint-disable max-lines */ +import type ApplicationInstance from '@ember/application/instance'; import { subscribe } from '@ember/instrumentation'; +import type Transition from '@ember/routing/-private/transition'; +import type RouterService from '@ember/routing/router-service'; +import { _backburner, run, scheduleOnce } from '@ember/runloop'; +import type { EmberRunQueues } from '@ember/runloop/-private/types'; +import { getOwnConfig, isTesting, macroCondition } from '@embroider/macros'; import * as Sentry from '@sentry/browser'; -import { ExtendedBackburner } from '@sentry/ember/runloop'; -import { Span, Transaction } from '@sentry/types'; -import { EmberRunQueues } from '@ember/runloop/-private/types'; -import { getActiveTransaction } from '..'; +import type { ExtendedBackburner } from '@sentry/ember/runloop'; +import type { Span, Transaction } from '@sentry/types'; import { browserPerformanceTimeOrigin, GLOBAL_OBJ, timestampInSeconds } from '@sentry/utils'; -import { macroCondition, isTesting, getOwnConfig } from '@embroider/macros'; -import { EmberSentryConfig, GlobalConfig, OwnConfig } from '../types'; -import RouterService from '@ember/routing/router-service'; -import type { BaseClient } from '@sentry/core'; -function getSentryConfig() { +import type { BrowserClient } from '..'; +import { getActiveTransaction } from '..'; +import type { EmberRouterMain, EmberSentryConfig, GlobalConfig, OwnConfig, StartTransactionFunction } from '../types'; + +type SentryTestRouterService = RouterService & { + _startTransaction?: StartTransactionFunction; + _sentryInstrumented?: boolean; +}; + +function getSentryConfig(): EmberSentryConfig { const _global = GLOBAL_OBJ as typeof GLOBAL_OBJ & GlobalConfig; _global.__sentryEmberConfig = _global.__sentryEmberConfig ?? {}; const environmentConfig = getOwnConfig().sentryConfig; @@ -27,7 +35,7 @@ function getSentryConfig() { export function initialize(appInstance: ApplicationInstance): void { // Disable in fastboot - we only want to run Sentry client-side - const fastboot = appInstance.lookup('service:fastboot') as { isFastBoot: boolean } | undefined; + const fastboot = appInstance.lookup('service:fastboot') as unknown as { isFastBoot: boolean } | undefined; if (fastboot?.isFastBoot) { return; } @@ -38,35 +46,42 @@ export function initialize(appInstance: ApplicationInstance): void { } const performancePromise = instrumentForPerformance(appInstance); if (macroCondition(isTesting())) { - (window)._sentryPerformanceLoad = performancePromise; + (window as typeof window & { _sentryPerformanceLoad?: Promise })._sentryPerformanceLoad = performancePromise; } } function getBackburner(): Pick { if (_backburner) { - return _backburner; + return _backburner as unknown as Pick; } - if (run.backburner) { - return run.backburner; + if ((run as unknown as { backburner?: Pick }).backburner) { + return (run as unknown as { backburner: Pick }).backburner; } return { - on() {}, - off() {}, + on() { + // noop + }, + off() { + // noop + }, }; } -function getTransitionInformation(transition: any, router: any) { +function getTransitionInformation( + transition: Transition | undefined, + router: RouterService, +): { fromRoute?: string; toRoute?: string } { const fromRoute = transition?.from?.name; - const toRoute = transition && transition.to ? transition.to.name : router.currentRouteName; + const toRoute = transition?.to?.name || router.currentRouteName; return { fromRoute, toRoute, }; } -function getLocationURL(location: any) { +function getLocationURL(location: EmberRouterMain['location']): string { if (!location || !location.getURL || !location.formatURL) { return ''; } @@ -79,22 +94,24 @@ function getLocationURL(location: any) { } export function _instrumentEmberRouter( - routerService: any, - routerMain: any, + routerService: RouterService, + routerMain: EmberRouterMain, config: EmberSentryConfig, - startTransaction: Function, + startTransaction: StartTransactionFunction, startTransactionOnPageLoad?: boolean, -) { +): { + startTransaction: StartTransactionFunction; +} { const { disableRunloopPerformance } = config; const location = routerMain.location; - let activeTransaction: Transaction; - let transitionSpan: Span; + let activeTransaction: Transaction | undefined; + let transitionSpan: Span | undefined; const url = getLocationURL(location); if (macroCondition(isTesting())) { - routerService._sentryInstrumented = true; - routerService._startTransaction = startTransaction; + (routerService as SentryTestRouterService)._sentryInstrumented = true; + (routerService as SentryTestRouterService)._startTransaction = startTransaction; } if (startTransactionOnPageLoad && url) { @@ -110,15 +127,15 @@ export function _instrumentEmberRouter( }); } - const finishActiveTransaction = function (_: any, nextInstance: any) { + const finishActiveTransaction = (_: unknown, nextInstance: unknown): void => { if (nextInstance) { return; } - activeTransaction.finish(); + activeTransaction?.finish(); getBackburner().off('end', finishActiveTransaction); }; - routerService.on('routeWillChange', (transition: any) => { + routerService.on('routeWillChange', (transition: Transition) => { const { fromRoute, toRoute } = getTransitionInformation(transition, routerService); activeTransaction?.finish(); activeTransaction = startTransaction({ @@ -130,7 +147,7 @@ export function _instrumentEmberRouter( 'routing.instrumentation': '@sentry/ember', }, }); - transitionSpan = activeTransaction.startChild({ + transitionSpan = activeTransaction?.startChild({ op: 'ui.ember.transition', description: `route:${fromRoute} -> route:${toRoute}`, }); @@ -155,7 +172,7 @@ export function _instrumentEmberRouter( }; } -function _instrumentEmberRunloop(config: EmberSentryConfig) { +function _instrumentEmberRunloop(config: EmberSentryConfig): void { const { disableRunloopPerformance, minimumRunloopQueueDuration } = config; if (disableRunloopPerformance) { return; @@ -171,7 +188,7 @@ function _instrumentEmberRunloop(config: EmberSentryConfig) { 'destroy', ] as EmberRunQueues[]; - getBackburner().on('begin', (_: any, previousInstance: any) => { + getBackburner().on('begin', (_: unknown, previousInstance: unknown) => { if (previousInstance) { return; } @@ -184,38 +201,38 @@ function _instrumentEmberRunloop(config: EmberSentryConfig) { } currentQueueStart = timestampInSeconds(); + const processQueue = (queue: EmberRunQueues): void => { + // Process this queue using the end of the previous queue. + if (currentQueueStart) { + const now = timestampInSeconds(); + const minQueueDuration = minimumRunloopQueueDuration ?? 5; + + if ((now - currentQueueStart) * 1000 >= minQueueDuration) { + activeTransaction + ?.startChild({ + op: `ui.ember.runloop.${queue}`, + startTimestamp: currentQueueStart, + endTimestamp: now, + }) + .finish(); + } + currentQueueStart = undefined; + } + + // Setup for next queue + + const stillActiveTransaction = getActiveTransaction(); + if (!stillActiveTransaction) { + return; + } + currentQueueStart = timestampInSeconds(); + }; + instrumentedEmberQueues.forEach(queue => { - scheduleOnce(queue, null, () => { - scheduleOnce(queue, null, () => { - // Process this queue using the end of the previous queue. - if (currentQueueStart) { - const now = timestampInSeconds(); - const minQueueDuration = minimumRunloopQueueDuration ?? 5; - - if ((now - currentQueueStart) * 1000 >= minQueueDuration) { - activeTransaction - ?.startChild({ - op: `ui.ember.runloop.${queue}`, - startTimestamp: currentQueueStart, - endTimestamp: now, - }) - .finish(); - } - currentQueueStart = undefined; - } - - // Setup for next queue - - const stillActiveTransaction = getActiveTransaction(); - if (!stillActiveTransaction) { - return; - } - currentQueueStart = timestampInSeconds(); - }); - }); + scheduleOnce(queue, null, processQueue, queue); }); }); - getBackburner().on('end', (_: any, nextInstance: any) => { + getBackburner().on('end', (_: unknown, nextInstance: unknown) => { if (nextInstance) { return; } @@ -241,7 +258,7 @@ interface RenderEntries { [name: string]: RenderEntry; } -function processComponentRenderBefore(payload: Payload, beforeEntries: RenderEntries) { +function processComponentRenderBefore(payload: Payload, beforeEntries: RenderEntries): void { const info = { payload, now: timestampInSeconds(), @@ -254,7 +271,7 @@ function processComponentRenderAfter( beforeEntries: RenderEntries, op: string, minComponentDuration: number, -) { +): void { const begin = beforeEntries[payload.object]; if (!begin) { @@ -276,7 +293,7 @@ function processComponentRenderAfter( } } -function _instrumentComponents(config: EmberSentryConfig) { +function _instrumentComponents(config: EmberSentryConfig): void { const { disableInstrumentComponents, minimumComponentRenderDuration, enableComponentDefinitions } = config; if (disableInstrumentComponents) { return; @@ -287,13 +304,13 @@ function _instrumentComponents(config: EmberSentryConfig) { const beforeEntries = {} as RenderEntries; const beforeComponentDefinitionEntries = {} as RenderEntries; - function _subscribeToRenderEvents() { + function _subscribeToRenderEvents(): void { subscribe('render.component', { before(_name: string, _timestamp: number, payload: Payload) { processComponentRenderBefore(payload, beforeEntries); }, - after(_name: string, _timestamp: number, payload: any, _beganIndex: number) { + after(_name: string, _timestamp: number, payload: Payload, _beganIndex: number) { processComponentRenderAfter(payload, beforeEntries, 'ui.ember.component.render', minComponentDuration); }, }); @@ -303,7 +320,7 @@ function _instrumentComponents(config: EmberSentryConfig) { processComponentRenderBefore(payload, beforeComponentDefinitionEntries); }, - after(_name: string, _timestamp: number, payload: any, _beganIndex: number) { + after(_name: string, _timestamp: number, payload: Payload, _beganIndex: number) { processComponentRenderAfter(payload, beforeComponentDefinitionEntries, 'ui.ember.component.definition', 0); }, }); @@ -312,11 +329,11 @@ function _instrumentComponents(config: EmberSentryConfig) { _subscribeToRenderEvents(); } -function _instrumentInitialLoad(config: EmberSentryConfig) { +function _instrumentInitialLoad(config: EmberSentryConfig): void { const startName = '@sentry/ember:initial-load-start'; const endName = '@sentry/ember:initial-load-end'; - let { HAS_PERFORMANCE, HAS_PERFORMANCE_TIMING } = _hasPerformanceSupport(); + const { HAS_PERFORMANCE, HAS_PERFORMANCE_TIMING } = _hasPerformanceSupport(); if (!HAS_PERFORMANCE) { return; @@ -331,7 +348,7 @@ function _instrumentInitialLoad(config: EmberSentryConfig) { } // Split performance check in two so clearMarks still happens even if timeOrigin isn't available. - if (!HAS_PERFORMANCE_TIMING) { + if (!HAS_PERFORMANCE_TIMING || browserPerformanceTimeOrigin === undefined) { return; } const measureName = '@sentry/ember:initial-load'; @@ -344,9 +361,10 @@ function _instrumentInitialLoad(config: EmberSentryConfig) { performance.measure(measureName, startName, endName); const measures = performance.getEntriesByName(measureName); - const measure = measures[0]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const measure = measures[0]!; - const startTimestamp = (measure.startTime + browserPerformanceTimeOrigin!) / 1000; + const startTimestamp = (measure.startTime + browserPerformanceTimeOrigin) / 1000; const endTimestamp = startTimestamp + measure.duration / 1000; const transaction = getActiveTransaction(); @@ -361,7 +379,7 @@ function _instrumentInitialLoad(config: EmberSentryConfig) { performance.clearMeasures(measureName); } -function _hasPerformanceSupport() { +function _hasPerformanceSupport(): { HAS_PERFORMANCE: boolean; HAS_PERFORMANCE_TIMING: boolean } { // TS says that all of these methods are always available, but some of them may not be supported in older browsers // So we "pretend" they are all optional in order to be able to check this properly without TS complaining const _performance = window.performance as { @@ -381,9 +399,8 @@ function _hasPerformanceSupport() { }; } -export async function instrumentForPerformance(appInstance: ApplicationInstance) { +export async function instrumentForPerformance(appInstance: ApplicationInstance): Promise { const config = getSentryConfig(); - const sentryConfig = config.sentry; // Maintaining backwards compatibility with config.browserTracingOptions, but passing it with Sentry options is preferred. const browserTracingOptions = config.browserTracingOptions || config.sentry.browserTracingOptions || {}; @@ -393,7 +410,8 @@ export async function instrumentForPerformance(appInstance: ApplicationInstance) const browserTracing = new BrowserTracing({ routingInstrumentation: (customStartTransaction, startTransactionOnPageLoad) => { - const routerMain = appInstance.lookup('router:main'); + // eslint-disable-next-line ember/no-private-routing-service + const routerMain = appInstance.lookup('router:main') as EmberRouterMain; let routerService = appInstance.lookup('service:router') as | RouterService & { externalRouter?: RouterService; _hasMountedSentryPerformanceRouting?: boolean }; @@ -421,8 +439,8 @@ export async function instrumentForPerformance(appInstance: ApplicationInstance) if ( client && - (client as BaseClient).getIntegrationById && - (client as BaseClient).getIntegrationById('BrowserTracing') + (client as BrowserClient).getIntegrationById && + (client as BrowserClient).getIntegrationById('BrowserTracing') ) { // Initializers are called more than once in tests, causing the integrations to not be setup correctly. return; diff --git a/packages/ember/addon/runloop.d.ts b/packages/ember/addon/runloop.d.ts index 7d1fbcf5949d..2e2964487b69 100644 --- a/packages/ember/addon/runloop.d.ts +++ b/packages/ember/addon/runloop.d.ts @@ -1,10 +1,10 @@ -import { Backburner } from '@ember/runloop/-private/backburner'; +import type { Backburner } from '@ember/runloop/-private/backburner'; /** * Backburner needs to be extended as it's missing the 'off' method. */ interface ExtendedBackburner extends Backburner { - off(...args: any[]): void; + off(...args: unknown[]): void; } /** diff --git a/packages/ember/addon/types.ts b/packages/ember/addon/types.ts index 7490bb2d8129..787eecc7e4cf 100644 --- a/packages/ember/addon/types.ts +++ b/packages/ember/addon/types.ts @@ -1,7 +1,10 @@ -import { BrowserOptions } from '@sentry/browser'; +import type { BrowserOptions, BrowserTracing } from '@sentry/browser'; +import type { Transaction, TransactionContext } from '@sentry/types'; + +type BrowserTracingOptions = ConstructorParameters[0]; export type EmberSentryConfig = { - sentry: BrowserOptions & { browserTracingOptions: Object }; + sentry: BrowserOptions & { browserTracingOptions?: BrowserTracingOptions }; transitionTimeout: number; ignoreEmberOnErrorWarning: boolean; disableInstrumentComponents: boolean; @@ -12,13 +15,25 @@ export type EmberSentryConfig = { enableComponentDefinitions: boolean; minimumRunloopQueueDuration: number; minimumComponentRenderDuration: number; - browserTracingOptions: Object; + browserTracingOptions: BrowserTracingOptions; }; export type OwnConfig = { sentryConfig: EmberSentryConfig; }; +// This is private in Ember and not really exported, so we "mock" these types here. +export interface EmberRouterMain { + location: { + getURL?: () => string; + formatURL?: (url: string) => string; + implementation: string; + rootURL: string; + }; +} + +export type StartTransactionFunction = (context: TransactionContext) => Transaction | undefined; + export type GlobalConfig = { __sentryEmberConfig: EmberSentryConfig['sentry']; }; diff --git a/packages/ember/index.js b/packages/ember/index.js index 53a6fb2eded7..de05e5d6089f 100644 --- a/packages/ember/index.js +++ b/packages/ember/index.js @@ -33,6 +33,7 @@ module.exports = { const addonConfig = config['@sentry/ember'] || {}; if (!isSerializable(addonConfig)) { + // eslint-disable-next-line no-console console.warn( `Warning: You passed a non-serializable config to \`ENV['@sentry/ember'].sentry\`. Non-serializable config (e.g. RegExp, ...) can only be passed directly to \`Sentry.init()\`, which is usually defined in app/app.js. @@ -73,6 +74,7 @@ function isSerializable(obj) { } if (isPlainObject(obj)) { + // eslint-disable-next-line guard-for-in for (let property in obj) { let value = obj[property]; if (!isSerializable(value)) { diff --git a/packages/ember/package.json b/packages/ember/package.json index 5a0da958779b..990f6bfbd82a 100644 --- a/packages/ember/package.json +++ b/packages/ember/package.json @@ -23,9 +23,14 @@ "lint:hbs": "ember-template-lint .", "lint:js": "eslint .", "lint:ts": "tsc", + "fix": "run-s fix:eslint fix:prettier", + "fix:eslint": "eslint . --format stylish --fix", + "fix:prettier": "prettier --write \"{addon,app,tests,config}/**/**.{ts,js}\"", "start": "ember serve", "test": "ember test", - "test:all": "ember try:each" + "test:all": "ember try:each", + "prepack": "ember ts:precompile", + "postpack": "ember ts:clean" }, "dependencies": { "@embroider/macros": "^1.9.0", @@ -39,14 +44,14 @@ }, "devDependencies": { "@ember/optional-features": "~1.3.0", - "@ember/test-helpers": "~2.8.1", + "@ember/test-helpers": "2.9.4", "@embroider/test-setup": "~1.8.3", "@glimmer/component": "~1.1.2", "@glimmer/tracking": "~1.1.2", "@types/ember": "~3.16.5", "@types/ember-qunit": "~3.4.9", "@types/ember__debug": "^3.16.5", - "@types/ember__test-helpers": "~1.7.0", + "@types/ember-resolver": "5.0.13", "@types/qunit": "~2.9.1", "@types/rsvp": "~4.0.3", "babel-eslint": "~10.1.0", @@ -69,9 +74,9 @@ "ember-test-selectors": "~6.0.0", "ember-try": "~2.0.0", "ember-window-mock": "~0.8.1", - "eslint-plugin-ember": "~11.1.0", - "eslint-plugin-node": "~11.1.0", - "eslint-plugin-qunit": "~7.3.1", + "eslint-plugin-ember": "11.9.0", + "eslint-plugin-n": "16.0.1", + "eslint-plugin-qunit": "8.0.0", "loader.js": "~4.7.0", "qunit": "~2.19.2", "qunit-dom": "~2.0.0", diff --git a/packages/ember/tests/acceptance/sentry-errors-test.js b/packages/ember/tests/acceptance/sentry-errors-test.js deleted file mode 100644 index 25f5510d34f7..000000000000 --- a/packages/ember/tests/acceptance/sentry-errors-test.js +++ /dev/null @@ -1,134 +0,0 @@ -import { test, module } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { find, click, visit } from '@ember/test-helpers'; -import { next } from '@ember/runloop'; -import { setupSentryTest } from '../helpers/setup-sentry'; - -const defaultAssertOptions = { - method: 'POST', - errorBodyContains: [], -}; - -function getTestSentryErrors() { - return window._sentryTestEvents.filter(event => event['type'] !== 'transaction'); -} - -function assertSentryErrorCount(assert, count) { - assert.equal(getTestSentryErrors().length, count, 'Check correct number of Sentry events were sent'); -} - -function assertSentryCall(assert, callNumber, options) { - const sentryTestEvents = getTestSentryErrors(); - const assertOptions = Object.assign({}, defaultAssertOptions, options); - - const event = sentryTestEvents[callNumber]; - - /** - * Body could be parsed here to check exact properties, but that requires too much implementation specific detail, - * instead this loosely matches on contents to check the correct error is being sent. - */ - assert.ok(assertOptions.errorBodyContains.length, 'Must pass strings to check against error body'); - const errorBody = JSON.stringify(event); - assertOptions.errorBodyContains.forEach(bodyContent => { - assert.ok(errorBody.includes(bodyContent), `Checking that error body includes ${bodyContent}`); - }); -} - -module('Acceptance | Sentry Errors', function (hooks) { - setupApplicationTest(hooks); - setupSentryTest(hooks); - - test('Check "Throw Generic Javascript Error"', async function (assert) { - assert.expect(3); - - await visit('/'); - const button = find('[data-test-button="Throw Generic Javascript Error"]'); - - await click(button); - - assertSentryErrorCount(assert, 1); - assertSentryCall(assert, 0, { errorBodyContains: [...this.errorMessages] }); - }); - - test('Check "Throw EmberError"', async function (assert) { - assert.expect(3); - - await visit('/'); - const button = find('[data-test-button="Throw EmberError"]'); - - await click(button); - - assertSentryErrorCount(assert, 1); - assertSentryCall(assert, 0, { errorBodyContains: [...this.errorMessages] }); - }); - - test('Check "Caught Thrown EmberError"', async function (assert) { - assert.expect(1); - - await visit('/'); - const button = find('[data-test-button="Caught Thrown EmberError"]'); - - await click(button); - - assertSentryErrorCount(assert, 0); - }); - - test('Check "Error From Fetch"', async function (assert) { - assert.expect(3); - - this.fetchStub.onFirstCall().callsFake((...args) => { - return this.fetchStub.callsThrough(args); - }); - await visit('/'); - const button = find('[data-test-button="Error From Fetch"]'); - - await click(button); - - const done = assert.async(); - - next(() => { - assertSentryErrorCount(assert, 1); - assertSentryCall(assert, 0, { errorBodyContains: [...this.errorMessages] }); - done(); - }); - }); - - test('Check "Error in AfterRender"', async function (assert) { - assert.expect(4); - - await visit('/'); - const button = find('[data-test-button="Error in AfterRender"]'); - - await click(button); - - assertSentryErrorCount(assert, 1); - assert.ok(this.qunitOnUnhandledRejection.calledOnce, 'Uncaught rejection should only be called once'); - assertSentryCall(assert, 0, { errorBodyContains: [...this.errorMessages] }); - }); - - test('Check "RSVP Rejection"', async function (assert) { - assert.expect(4); - - await visit('/'); - const button = find('[data-test-button="RSVP Rejection"]'); - - await click(button); - - assertSentryErrorCount(assert, 1); - assert.ok(this.qunitOnUnhandledRejection.calledOnce, 'Uncaught rejection should only be called once'); - assertSentryCall(assert, 0, { errorBodyContains: [this.qunitOnUnhandledRejection.getCall(0).args[0]] }); - }); - - test('Check "Error inside RSVP"', async function (assert) { - assert.expect(4); - - await visit('/'); - const button = find('[data-test-button="Error inside RSVP"]'); - - await click(button); - - assertSentryErrorCount(assert, 1); - assert.ok(this.qunitOnUnhandledRejection.calledOnce, 'Uncaught rejection should only be called once'); - assertSentryCall(assert, 0, { errorBodyContains: [...this.errorMessages] }); - }); -}); diff --git a/packages/ember/tests/acceptance/sentry-errors-test.ts b/packages/ember/tests/acceptance/sentry-errors-test.ts new file mode 100644 index 000000000000..193f56e495cf --- /dev/null +++ b/packages/ember/tests/acceptance/sentry-errors-test.ts @@ -0,0 +1,101 @@ +import { next } from '@ember/runloop'; +import { click, visit } from '@ember/test-helpers'; +import { setupApplicationTest } from 'ember-qunit'; +import { module, test } from 'qunit'; + +import type { SentryTestContext } from '../helpers/setup-sentry'; +import { setupSentryTest } from '../helpers/setup-sentry'; +import { assertSentryErrorCount, assertSentryErrors } from '../helpers/utils'; + +module('Acceptance | Sentry Errors', function (hooks) { + setupApplicationTest(hooks); + setupSentryTest(hooks); + + test('Check "Throw Generic Javascript Error"', async function (this: SentryTestContext, assert) { + assert.expect(3); + + await visit('/'); + + await click('[data-test-button="Throw Generic Javascript Error"]'); + + assertSentryErrorCount(assert, 1); + assertSentryErrors(assert, 0, { errorBodyContains: [...this.errorMessages] }); + }); + + test('Check "Throw EmberError"', async function (this: SentryTestContext, assert) { + assert.expect(3); + + await visit('/'); + + await click('[data-test-button="Throw EmberError"]'); + + assertSentryErrorCount(assert, 1); + assertSentryErrors(assert, 0, { errorBodyContains: [...this.errorMessages] }); + }); + + test('Check "Caught Thrown EmberError"', async function (this: SentryTestContext, assert) { + assert.expect(1); + + await visit('/'); + + await click('[data-test-button="Caught Thrown EmberError"]'); + + assertSentryErrorCount(assert, 0); + }); + + test('Check "Error From Fetch"', async function (this: SentryTestContext, assert) { + assert.expect(3); + + this.fetchStub.onFirstCall().callsFake(() => { + throw new Error('Test error...'); + }); + + await visit('/'); + + await click('[data-test-button="Error From Fetch"]'); + + const done = assert.async(); + + next(() => { + assertSentryErrorCount(assert, 1); + assertSentryErrors(assert, 0, { errorBodyContains: ['Test error...'] }); + done(); + }); + }); + + test('Check "Error in AfterRender"', async function (this: SentryTestContext, assert) { + assert.expect(4); + + await visit('/'); + + await click('[data-test-button="Error in AfterRender"]'); + + assertSentryErrorCount(assert, 1); + assert.ok(this.qunitOnUnhandledRejection.calledOnce, 'Uncaught rejection should only be called once'); + assertSentryErrors(assert, 0, { errorBodyContains: [...this.errorMessages] }); + }); + + test('Check "RSVP Rejection"', async function (this: SentryTestContext, assert) { + assert.expect(4); + + await visit('/'); + + await click('[data-test-button="RSVP Rejection"]'); + + assertSentryErrorCount(assert, 1); + assert.ok(this.qunitOnUnhandledRejection.calledOnce, 'Uncaught rejection should only be called once'); + assertSentryErrors(assert, 0, { errorBodyContains: [this.qunitOnUnhandledRejection.getCall(0).args[0]] }); + }); + + test('Check "Error inside RSVP"', async function (this: SentryTestContext, assert) { + assert.expect(4); + + await visit('/'); + + await click('[data-test-button="Error inside RSVP"]'); + + assertSentryErrorCount(assert, 1); + assert.ok(this.qunitOnUnhandledRejection.calledOnce, 'Uncaught rejection should only be called once'); + assertSentryErrors(assert, 0, { errorBodyContains: [...this.errorMessages] }); + }); +}); diff --git a/packages/ember/tests/acceptance/sentry-performance-test.js b/packages/ember/tests/acceptance/sentry-performance-test.js deleted file mode 100644 index a1be11440217..000000000000 --- a/packages/ember/tests/acceptance/sentry-performance-test.js +++ /dev/null @@ -1,104 +0,0 @@ -import { test, module } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { find, click, visit } from '@ember/test-helpers'; -import { setupSentryTest } from '../helpers/setup-sentry'; - -const SLOW_TRANSITION_WAIT = 3000; - -function getTestSentryTransactions() { - return window._sentryTestEvents.filter(event => event['type'] === 'transaction'); -} - -function assertSentryTransactionCount(assert, count) { - assert.equal(getTestSentryTransactions().length, count, 'Check correct number of Sentry events were sent'); -} - -function assertSentryCall(assert, callNumber, options) { - const sentryTestEvents = getTestSentryTransactions(); - const event = sentryTestEvents[callNumber]; - - assert.ok(options.spanCount || options.spans, 'Must add spanCount or spans to assertion'); - if (options.spanCount) { - assert.equal(event.spans.length, options.spanCount); - } - if (options.spans) { - // instead of checking the specific order of runloop spans (which is brittle), - // we check (below) that _any_ runloop spans are added - const spans = event.spans - .filter(span => !span.op.startsWith('ui.ember.runloop.')) - .map(s => { - return `${s.op} | ${s.description}`; - }); - - assert.true( - event.spans.some(span => span.op.startsWith('ui.ember.runloop.')), - 'it captures runloop spans', - ); - assert.deepEqual(spans, options.spans, `Has correct spans`); - } - - assert.equal(event.transaction, options.transaction); - assert.equal(event.tags.fromRoute, options.tags.fromRoute); - assert.equal(event.tags.toRoute, options.tags.toRoute); - - if (options.durationCheck) { - const duration = (event.timestamp - event.start_timestamp) * 1000; - assert.ok(options.durationCheck(duration), `duration (${duration}ms) passes duration check`); - } -} - -module('Acceptance | Sentry Performance', function (hooks) { - setupApplicationTest(hooks); - setupSentryTest(hooks); - - test('Test transaction', async function (assert) { - assert.expect(7); - - await visit('/tracing'); - - assertSentryTransactionCount(assert, 1); - assertSentryCall(assert, 0, { - spans: [ - 'ui.ember.transition | route:undefined -> route:tracing', - 'ui.ember.component.render | component:test-section', - ], - transaction: 'route:tracing', - tags: { - fromRoute: undefined, - toRoute: 'tracing', - }, - }); - }); - - test('Test navigating to slow route', async function (assert) { - assert.expect(8); - - await visit('/tracing'); - const button = find('[data-test-button="Transition to slow loading route"]'); - - await click(button); - - assertSentryTransactionCount(assert, 2); - assertSentryCall(assert, 1, { - spans: [ - 'ui.ember.transition | route:tracing -> route:slow-loading-route.index', - 'ui.ember.route.before_model | slow-loading-route', - 'ui.ember.route.model | slow-loading-route', - 'ui.ember.route.after_model | slow-loading-route', - 'ui.ember.route.before_model | slow-loading-route.index', - 'ui.ember.route.model | slow-loading-route.index', - 'ui.ember.route.after_model | slow-loading-route.index', - 'ui.ember.route.setup_controller | slow-loading-route', - 'ui.ember.route.setup_controller | slow-loading-route.index', - 'ui.ember.component.render | component:slow-loading-list', - 'ui.ember.component.render | component:slow-loading-list', - ], - transaction: 'route:slow-loading-route.index', - durationCheck: duration => duration > SLOW_TRANSITION_WAIT, - tags: { - fromRoute: 'tracing', - toRoute: 'slow-loading-route.index', - }, - }); - }); -}); diff --git a/packages/ember/tests/acceptance/sentry-performance-test.ts b/packages/ember/tests/acceptance/sentry-performance-test.ts new file mode 100644 index 000000000000..7ee5ccfcc14a --- /dev/null +++ b/packages/ember/tests/acceptance/sentry-performance-test.ts @@ -0,0 +1,59 @@ +import { click, visit } from '@ember/test-helpers'; +import { setupApplicationTest } from 'ember-qunit'; +import { module, test } from 'qunit'; + +import { setupSentryTest } from '../helpers/setup-sentry'; +import { assertSentryTransactionCount, assertSentryTransactions } from '../helpers/utils'; + +const SLOW_TRANSITION_WAIT = 3000; + +module('Acceptance | Sentry Performance', function (hooks) { + setupApplicationTest(hooks); + setupSentryTest(hooks); + + test('Test transaction', async function (assert) { + await visit('/tracing'); + + assertSentryTransactionCount(assert, 1); + assertSentryTransactions(assert, 0, { + spans: [ + 'ui.ember.transition | route:undefined -> route:tracing', + 'ui.ember.component.render | component:test-section', + ], + transaction: 'route:tracing', + tags: { + fromRoute: undefined, + toRoute: 'tracing', + }, + }); + }); + + test('Test navigating to slow route', async function (assert) { + await visit('/tracing'); + + await click('[data-test-button="Transition to slow loading route"]'); + + assertSentryTransactionCount(assert, 2); + assertSentryTransactions(assert, 1, { + spans: [ + 'ui.ember.transition | route:tracing -> route:slow-loading-route.index', + 'ui.ember.route.before_model | slow-loading-route', + 'ui.ember.route.model | slow-loading-route', + 'ui.ember.route.after_model | slow-loading-route', + 'ui.ember.route.before_model | slow-loading-route.index', + 'ui.ember.route.model | slow-loading-route.index', + 'ui.ember.route.after_model | slow-loading-route.index', + 'ui.ember.route.setup_controller | slow-loading-route', + 'ui.ember.route.setup_controller | slow-loading-route.index', + 'ui.ember.component.render | component:slow-loading-list', + 'ui.ember.component.render | component:slow-loading-list', + ], + transaction: 'route:slow-loading-route.index', + durationCheck: duration => duration > SLOW_TRANSITION_WAIT, + tags: { + fromRoute: 'tracing', + toRoute: 'slow-loading-route.index', + }, + }); + }); +}); diff --git a/packages/ember/tests/acceptance/sentry-replay-test.js b/packages/ember/tests/acceptance/sentry-replay-test.ts similarity index 51% rename from packages/ember/tests/acceptance/sentry-replay-test.js rename to packages/ember/tests/acceptance/sentry-replay-test.ts index f6c7272a7e62..1614dc22817a 100644 --- a/packages/ember/tests/acceptance/sentry-replay-test.js +++ b/packages/ember/tests/acceptance/sentry-replay-test.ts @@ -1,8 +1,10 @@ -import { test, module } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; import { visit } from '@ember/test-helpers'; -import { setupSentryTest } from '../helpers/setup-sentry'; import * as Sentry from '@sentry/ember'; +import type { ReplayContainer } from '@sentry/replay/build/npm/types/types'; +import { setupApplicationTest } from 'ember-qunit'; +import { module, test } from 'qunit'; + +import { setupSentryTest } from '../helpers/setup-sentry'; module('Acceptance | Sentry Session Replay', function (hooks) { setupApplicationTest(hooks); @@ -11,10 +13,12 @@ module('Acceptance | Sentry Session Replay', function (hooks) { test('Test replay', async function (assert) { await visit('/replay'); - const replay = Sentry.getCurrentHub().getIntegration(Sentry.Replay); - assert.ok(replay); + const integration = Sentry.getCurrentHub().getIntegration(Sentry.Replay); + assert.ok(integration); + + const replay = (integration as Sentry.Replay)['_replay'] as ReplayContainer; - assert.true(replay._replay.isEnabled()); - assert.false(replay._replay.isPaused()); + assert.true(replay.isEnabled()); + assert.false(replay.isPaused()); }); }); diff --git a/packages/ember/tests/dummy/app/app.js b/packages/ember/tests/dummy/app/app.ts similarity index 60% rename from packages/ember/tests/dummy/app/app.js rename to packages/ember/tests/dummy/app/app.ts index 322931b8d45a..2ff7875df400 100644 --- a/packages/ember/tests/dummy/app/app.js +++ b/packages/ember/tests/dummy/app/app.ts @@ -1,24 +1,19 @@ import Application from '@ember/application'; -import Resolver from 'ember-resolver'; +import * as Sentry from '@sentry/ember'; import loadInitializers from 'ember-load-initializers'; +import Resolver from 'ember-resolver'; + import config from './config/environment'; -import * as Sentry from '@sentry/ember'; Sentry.init({ replaysSessionSampleRate: 1, replaysOnErrorSampleRate: 1, - browserTracingOptions: { - _experiments: { - // This lead to some flaky tests, as that is sometimes logged - enableLongTask: false, - }, - }, }); export default class App extends Application { - modulePrefix = config.modulePrefix; - podModulePrefix = config.podModulePrefix; - Resolver = Resolver; + public modulePrefix = config.modulePrefix; + public podModulePrefix = config.podModulePrefix; + public Resolver = Resolver; } loadInitializers(App, config.modulePrefix); diff --git a/packages/ember/tests/dummy/app/components/link.ts b/packages/ember/tests/dummy/app/components/link.ts index e67158350840..1ba66df216fc 100644 --- a/packages/ember/tests/dummy/app/components/link.ts +++ b/packages/ember/tests/dummy/app/components/link.ts @@ -1,7 +1,7 @@ -import Component from '@glimmer/component'; -import RouterService from '@ember/routing/router-service'; -import { inject as service } from '@ember/service'; import { action } from '@ember/object'; +import type RouterService from '@ember/routing/router-service'; +import { inject as service } from '@ember/service'; +import Component from '@glimmer/component'; interface Args { route: string; @@ -14,20 +14,20 @@ interface Args { Since glimmer components are, as of now, not instrumented, this leads to different test results. */ export default class LinkComponent extends Component { - @service router: RouterService; + @service public declare router: RouterService; - get href() { + public get href(): string { return this.router.urlFor(this.args.route); } - get isActive() { + public get isActive(): boolean { return this.router.currentRouteName === this.args.route; } @action - onClick(event: MouseEvent) { + public onClick(event: MouseEvent): void { event.preventDefault(); - this.router.transitionTo(this.args.route); + void this.router.transitionTo(this.args.route); } } diff --git a/packages/ember/tests/dummy/app/components/slow-loading-gc-list.ts b/packages/ember/tests/dummy/app/components/slow-loading-gc-list.ts index c5205d47e4f6..3ac89dc43ca7 100644 --- a/packages/ember/tests/dummy/app/components/slow-loading-gc-list.ts +++ b/packages/ember/tests/dummy/app/components/slow-loading-gc-list.ts @@ -1,3 +1,4 @@ +/* eslint-disable ember/no-empty-glimmer-component-classes */ import Component from '@glimmer/component'; export default class SlowLoadingGCList extends Component {} diff --git a/packages/ember/tests/dummy/app/components/slow-loading-list.ts b/packages/ember/tests/dummy/app/components/slow-loading-list.ts index 9ef19af6da88..e766fe78609f 100644 --- a/packages/ember/tests/dummy/app/components/slow-loading-list.ts +++ b/packages/ember/tests/dummy/app/components/slow-loading-list.ts @@ -1,12 +1,22 @@ +/* eslint-disable ember/no-classic-classes */ +/* eslint-disable ember/no-classic-components */ import Component from '@ember/component'; import { computed } from '@ember/object'; +interface Args { + title?: string; + items: number; +} + export default Component.extend({ + tagName: '', + _title: computed('title', function () { - return this.title || 'Slow Loading List'; + return (this as Args).title || 'Slow Loading List'; }), + rowItems: computed('items', function () { - return new Array(parseInt(this.items)).fill(0).map((_, index) => { + return new Array((this as Args).items).fill(0).map((_, index) => { return { index: index + 1, }; diff --git a/packages/ember/tests/dummy/app/components/test-section.ts b/packages/ember/tests/dummy/app/components/test-section.ts index 55706477346c..d0ca7e8edabc 100644 --- a/packages/ember/tests/dummy/app/components/test-section.ts +++ b/packages/ember/tests/dummy/app/components/test-section.ts @@ -1,3 +1,6 @@ +/* eslint-disable ember/no-classic-classes */ +/* eslint-disable ember/no-classic-components */ +/* eslint-disable ember/require-tagless-components */ import Component from '@ember/component'; export default Component.extend({}); diff --git a/packages/ember/tests/dummy/app/config/environment.d.ts b/packages/ember/tests/dummy/app/config/environment.d.ts index 3252cc3dec43..8a8a687909e4 100644 --- a/packages/ember/tests/dummy/app/config/environment.d.ts +++ b/packages/ember/tests/dummy/app/config/environment.d.ts @@ -1,5 +1,3 @@ -export default config; - /** * Type declarations for * import config from './config/environment' @@ -8,9 +6,12 @@ export default config; * since different ember addons can materialize new entries. */ declare const config: { - environment: any; + environment: string; modulePrefix: string; podModulePrefix: string; - locationType: string; + locationType: 'history' | 'hash' | 'none' | 'auto'; rootURL: string; + APP: Record; }; + +export default config; diff --git a/packages/ember/tests/dummy/app/controllers/application.js b/packages/ember/tests/dummy/app/controllers/application.js deleted file mode 100644 index 304707936f81..000000000000 --- a/packages/ember/tests/dummy/app/controllers/application.js +++ /dev/null @@ -1,3 +0,0 @@ -import Controller from '@ember/controller'; - -export default class ApplicationController extends Controller {} diff --git a/packages/ember/tests/dummy/app/controllers/index.js b/packages/ember/tests/dummy/app/controllers/index.ts similarity index 54% rename from packages/ember/tests/dummy/app/controllers/index.js rename to packages/ember/tests/dummy/app/controllers/index.ts index a1e01de99a8c..bd28b635e1a1 100644 --- a/packages/ember/tests/dummy/app/controllers/index.js +++ b/packages/ember/tests/dummy/app/controllers/index.ts @@ -1,63 +1,64 @@ import Controller from '@ember/controller'; -import { tracked } from '@glimmer/tracking'; -import { action } from '@ember/object'; import EmberError from '@ember/error'; +import { action } from '@ember/object'; import { scheduleOnce } from '@ember/runloop'; -import RSVP from 'rsvp'; +import { tracked } from '@glimmer/tracking'; +import { Promise } from 'rsvp'; export default class IndexController extends Controller { - @tracked showComponents; + @tracked public showComponents = false; @action - createError() { + public createError(): void { + // @ts-expect-error this is fine this.nonExistentFunction(); } @action - createEmberError() { + public createEmberError(): void { throw new EmberError('Whoops, looks like you have an EmberError'); } @action - createCaughtEmberError() { + public createCaughtEmberError(): void { try { throw new EmberError('Looks like you have a caught EmberError'); } catch (e) { - console.log(e); + // do nothing } } @action - createFetchError() { - fetch('http://doesntexist.example'); + public createFetchError(): void { + void fetch('http://doesntexist.example'); } @action - createAfterRenderError() { - function throwAfterRender() { + public createAfterRenderError(): void { + function throwAfterRender(): void { throw new Error('After Render Error'); } - scheduleOnce('afterRender', throwAfterRender); + scheduleOnce('afterRender', null, throwAfterRender); } @action - createRSVPRejection() { - const promise = new RSVP.Promise((resolve, reject) => { + public createRSVPRejection(): Promise { + const promise = new Promise((resolve, reject) => { reject('Promise rejected'); }); return promise; } @action - createRSVPError() { - const promise = new RSVP.Promise(() => { + public createRSVPError(): Promise { + const promise = new Promise(() => { throw new Error('Error within RSVP Promise'); }); return promise; } @action - toggleShowComponents() { + public toggleShowComponents(): void { this.showComponents = !this.showComponents; } } diff --git a/packages/ember/tests/dummy/app/controllers/slow-loading-route.js b/packages/ember/tests/dummy/app/controllers/slow-loading-route.js deleted file mode 100644 index afe644bffd41..000000000000 --- a/packages/ember/tests/dummy/app/controllers/slow-loading-route.js +++ /dev/null @@ -1,9 +0,0 @@ -import Controller from '@ember/controller'; -import { action } from '@ember/object'; - -export default class SlowLoadingRouteController extends Controller { - @action - back() { - this.transitionToRoute('tracing'); - } -} diff --git a/packages/ember/tests/dummy/app/controllers/slow-loading-route.ts b/packages/ember/tests/dummy/app/controllers/slow-loading-route.ts new file mode 100644 index 000000000000..01a523ea0985 --- /dev/null +++ b/packages/ember/tests/dummy/app/controllers/slow-loading-route.ts @@ -0,0 +1,13 @@ +import Controller from '@ember/controller'; +import { action } from '@ember/object'; +import type RouterService from '@ember/routing/router-service'; +import { inject as service } from '@ember/service'; + +export default class SlowLoadingRouteController extends Controller { + @service public declare router: RouterService; + + @action + public back(): void { + void this.router.transitionTo('tracing'); + } +} diff --git a/packages/ember/tests/dummy/app/controllers/slow-loading-route/index.js b/packages/ember/tests/dummy/app/controllers/slow-loading-route/index.ts similarity index 56% rename from packages/ember/tests/dummy/app/controllers/slow-loading-route/index.js rename to packages/ember/tests/dummy/app/controllers/slow-loading-route/index.ts index 2c712dd18032..b66350b5c911 100644 --- a/packages/ember/tests/dummy/app/controllers/slow-loading-route/index.js +++ b/packages/ember/tests/dummy/app/controllers/slow-loading-route/index.ts @@ -1,5 +1,5 @@ import Controller from '@ember/controller'; export default class SlowLoadingRouteController extends Controller { - slowLoadingTemplateOnlyItems = new Array(2000).fill(0).map((_, index) => index); + public slowLoadingTemplateOnlyItems = new Array(2000).fill(0).map((_, index) => index); } diff --git a/packages/ember/tests/dummy/app/controllers/tracing.js b/packages/ember/tests/dummy/app/controllers/tracing.js deleted file mode 100644 index 539a989ac829..000000000000 --- a/packages/ember/tests/dummy/app/controllers/tracing.js +++ /dev/null @@ -1,9 +0,0 @@ -import Controller from '@ember/controller'; -import { action } from '@ember/object'; - -export default class TracingController extends Controller { - @action - navigateToSlowRoute() { - return this.transitionToRoute('slow-loading-route'); - } -} diff --git a/packages/ember/tests/dummy/app/controllers/tracing.ts b/packages/ember/tests/dummy/app/controllers/tracing.ts new file mode 100644 index 000000000000..72c0d635702e --- /dev/null +++ b/packages/ember/tests/dummy/app/controllers/tracing.ts @@ -0,0 +1,13 @@ +import Controller from '@ember/controller'; +import { action } from '@ember/object'; +import type RouterService from '@ember/routing/router-service'; +import { inject as service } from '@ember/service'; + +export default class TracingController extends Controller { + @service public declare router: RouterService; + + @action + public navigateToSlowRoute(): void { + void this.router.transitionTo('slow-loading-route'); + } +} diff --git a/packages/ember/tests/dummy/app/helpers/utils.js b/packages/ember/tests/dummy/app/helpers/utils.js deleted file mode 100644 index 7aa94999942c..000000000000 --- a/packages/ember/tests/dummy/app/helpers/utils.js +++ /dev/null @@ -1,3 +0,0 @@ -export default function timeout(time) { - return new Promise(resolve => setTimeout(resolve, time)); -} diff --git a/packages/ember/tests/dummy/app/helpers/utils.ts b/packages/ember/tests/dummy/app/helpers/utils.ts new file mode 100644 index 000000000000..60a3f2956224 --- /dev/null +++ b/packages/ember/tests/dummy/app/helpers/utils.ts @@ -0,0 +1,3 @@ +export default function timeout(time: number): Promise { + return new Promise(resolve => setTimeout(resolve, time)); +} diff --git a/packages/ember/tests/dummy/app/initializers/deprecation.js b/packages/ember/tests/dummy/app/initializers/deprecation.ts similarity index 79% rename from packages/ember/tests/dummy/app/initializers/deprecation.js rename to packages/ember/tests/dummy/app/initializers/deprecation.ts index 33c16ee62d4e..190b1a932397 100644 --- a/packages/ember/tests/dummy/app/initializers/deprecation.js +++ b/packages/ember/tests/dummy/app/initializers/deprecation.ts @@ -1,10 +1,11 @@ import { registerDeprecationHandler } from '@ember/debug'; -export function initialize() { +export function initialize(): void { registerDeprecationHandler((message, options, next) => { if (options && options.until && options.until !== '3.0.0') { return; } else { + // @ts-expect-error this is fine next(message, options); } }); diff --git a/packages/ember/tests/dummy/app/models/.gitkeep b/packages/ember/tests/dummy/app/models/.gitkeep deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/packages/ember/tests/dummy/app/router.js b/packages/ember/tests/dummy/app/router.ts similarity index 64% rename from packages/ember/tests/dummy/app/router.js rename to packages/ember/tests/dummy/app/router.ts index 51b41107869e..e13dec6d82c5 100644 --- a/packages/ember/tests/dummy/app/router.js +++ b/packages/ember/tests/dummy/app/router.ts @@ -1,11 +1,14 @@ import EmberRouter from '@ember/routing/router'; + import config from './config/environment'; export default class Router extends EmberRouter { - location = config.locationType; - rootURL = config.rootURL; + public location = config.locationType; + public rootURL = config.rootURL; } +// This is a false positive of the eslint rule +// eslint-disable-next-line array-callback-return Router.map(function () { this.route('tracing'); this.route('replay'); diff --git a/packages/ember/tests/dummy/app/routes/replay.js b/packages/ember/tests/dummy/app/routes/replay.ts similarity index 61% rename from packages/ember/tests/dummy/app/routes/replay.js rename to packages/ember/tests/dummy/app/routes/replay.ts index ed3a95e9cee7..9702fe8aa1af 100644 --- a/packages/ember/tests/dummy/app/routes/replay.js +++ b/packages/ember/tests/dummy/app/routes/replay.ts @@ -1,12 +1,13 @@ import Route from '@ember/routing/route'; +import type { BrowserClient } from '@sentry/ember'; import * as Sentry from '@sentry/ember'; export default class ReplayRoute extends Route { - async beforeModel() { + public async beforeModel(): Promise { const { Replay } = Sentry; if (!Sentry.getCurrentHub().getIntegration(Replay)) { - const client = Sentry.getCurrentHub().getClient(); + const client = Sentry.getCurrentHub().getClient() as BrowserClient; client.addIntegration(new Replay()); } } diff --git a/packages/ember/tests/dummy/app/routes/slow-loading-route.js b/packages/ember/tests/dummy/app/routes/slow-loading-route.ts similarity index 63% rename from packages/ember/tests/dummy/app/routes/slow-loading-route.js rename to packages/ember/tests/dummy/app/routes/slow-loading-route.ts index a28b0fd7eff6..96f57bd9cf2d 100644 --- a/packages/ember/tests/dummy/app/routes/slow-loading-route.js +++ b/packages/ember/tests/dummy/app/routes/slow-loading-route.ts @@ -1,24 +1,25 @@ import Route from '@ember/routing/route'; -import timeout from '../helpers/utils'; import { instrumentRoutePerformance } from '@sentry/ember'; +import timeout from '../helpers/utils'; + const SLOW_TRANSITION_WAIT = 1500; class SlowDefaultLoadingRoute extends Route { - beforeModel() { + public beforeModel(): Promise { return timeout(SLOW_TRANSITION_WAIT / 3); } - model() { + public model(): Promise { return timeout(SLOW_TRANSITION_WAIT / 3); } - afterModel() { + public afterModel(): Promise { return timeout(SLOW_TRANSITION_WAIT / 3); } - setupController() { - super.setupController(); + public setupController(...rest: Parameters): ReturnType { + super.setupController(...rest); } } diff --git a/packages/ember/tests/dummy/app/routes/slow-loading-route/index.js b/packages/ember/tests/dummy/app/routes/slow-loading-route/index.ts similarity index 62% rename from packages/ember/tests/dummy/app/routes/slow-loading-route/index.js rename to packages/ember/tests/dummy/app/routes/slow-loading-route/index.ts index 658fe8680bc8..c810ca5e2505 100644 --- a/packages/ember/tests/dummy/app/routes/slow-loading-route/index.js +++ b/packages/ember/tests/dummy/app/routes/slow-loading-route/index.ts @@ -1,24 +1,25 @@ import Route from '@ember/routing/route'; -import timeout from '../../helpers/utils'; import { instrumentRoutePerformance } from '@sentry/ember'; +import timeout from '../../helpers/utils'; + const SLOW_TRANSITION_WAIT = 1500; class SlowLoadingRoute extends Route { - beforeModel() { + public beforeModel(): Promise { return timeout(SLOW_TRANSITION_WAIT / 3); } - model() { + public model(): Promise { return timeout(SLOW_TRANSITION_WAIT / 3); } - afterModel() { + public afterModel(): Promise { return timeout(SLOW_TRANSITION_WAIT / 3); } - setupController() { - super.setupController(); + public setupController(...rest: Parameters): ReturnType { + super.setupController(...rest); } } diff --git a/packages/ember/tests/dummy/config/environment.js b/packages/ember/tests/dummy/config/environment.js index d80432e025b6..70c45d17ff0c 100644 --- a/packages/ember/tests/dummy/config/environment.js +++ b/packages/ember/tests/dummy/config/environment.js @@ -26,6 +26,10 @@ module.exports = function (environment) { dsn: process.env.SENTRY_DSN || 'https://0@0.ingest.sentry.io/0', browserTracingOptions: { tracingOrigins: ['localhost', 'doesntexist.example'], + _experiments: { + // This lead to some flaky tests, as that is sometimes logged + enableLongTask: false, + }, }, }, ignoreEmberOnErrorWarning: true, diff --git a/packages/ember/tests/dummy/constants.js b/packages/ember/tests/dummy/constants.ts similarity index 100% rename from packages/ember/tests/dummy/constants.js rename to packages/ember/tests/dummy/constants.ts diff --git a/packages/ember/tests/helpers/index.js b/packages/ember/tests/helpers/index.js deleted file mode 100644 index 7f70de80f4d4..000000000000 --- a/packages/ember/tests/helpers/index.js +++ /dev/null @@ -1,42 +0,0 @@ -import { - setupApplicationTest as upstreamSetupApplicationTest, - setupRenderingTest as upstreamSetupRenderingTest, - setupTest as upstreamSetupTest, -} from 'ember-qunit'; - -// This file exists to provide wrappers around ember-qunit's / ember-mocha's -// test setup functions. This way, you can easily extend the setup that is -// needed per test type. - -function setupApplicationTest(hooks, options) { - upstreamSetupApplicationTest(hooks, options); - - // Additional setup for application tests can be done here. - // - // For example, if you need an authenticated session for each - // application test, you could do: - // - // hooks.beforeEach(async function () { - // await authenticateSession(); // ember-simple-auth - // }); - // - // This is also a good place to call test setup functions coming - // from other addons: - // - // setupIntl(hooks); // ember-intl - // setupMirage(hooks); // ember-cli-mirage -} - -function setupRenderingTest(hooks, options) { - upstreamSetupRenderingTest(hooks, options); - - // Additional setup for rendering tests can be done here. -} - -function setupTest(hooks, options) { - upstreamSetupTest(hooks, options); - - // Additional setup for unit tests can be done here. -} - -export { setupApplicationTest, setupRenderingTest, setupTest }; diff --git a/packages/ember/tests/helpers/setup-sentry.js b/packages/ember/tests/helpers/setup-sentry.ts similarity index 53% rename from packages/ember/tests/helpers/setup-sentry.js rename to packages/ember/tests/helpers/setup-sentry.ts index a9e241c5d41a..d8bb513dcd00 100644 --- a/packages/ember/tests/helpers/setup-sentry.js +++ b/packages/ember/tests/helpers/setup-sentry.ts @@ -1,26 +1,41 @@ -import sinon from 'sinon'; -import { _instrumentEmberRouter } from '@sentry/ember/instance-initializers/sentry-performance'; +import type RouterService from '@ember/routing/router-service'; +import type { TestContext } from '@ember/test-helpers'; import { resetOnerror, setupOnerror } from '@ember/test-helpers'; +import { _instrumentEmberRouter } from '@sentry/ember/instance-initializers/sentry-performance'; +import type { EmberRouterMain, EmberSentryConfig, StartTransactionFunction } from '@sentry/ember/types'; +import sinon from 'sinon'; // Keep a reference to the original startTransaction as the application gets re-initialized and setup for // the integration doesn't occur again after the first time. -let _routerStartTransaction; +let _routerStartTransaction: StartTransactionFunction | undefined; + +export type SentryTestContext = TestContext & { + errorMessages: string[]; + fetchStub: sinon.SinonStub; + qunitOnUnhandledRejection: sinon.SinonStub; + _windowOnError: OnErrorEventHandler; +}; + +type SentryRouterService = RouterService & { + _startTransaction: StartTransactionFunction; + _sentryInstrumented?: boolean; +}; -export function setupSentryTest(hooks) { - hooks.beforeEach(async function () { +export function setupSentryTest(hooks: NestedHooks): void { + hooks.beforeEach(async function (this: SentryTestContext) { await window._sentryPerformanceLoad; window._sentryTestEvents = []; - const errorMessages = []; + const errorMessages: string[] = []; this.errorMessages = errorMessages; // eslint-disable-next-line ember/no-private-routing-service - const routerMain = this.owner.lookup('router:main'); - const routerService = this.owner.lookup('service:router'); + const routerMain = this.owner.lookup('router:main') as EmberRouterMain; + const routerService = this.owner.lookup('service:router') as SentryRouterService; if (routerService._sentryInstrumented) { _routerStartTransaction = routerService._startTransaction; - } else { - _instrumentEmberRouter(routerService, routerMain, {}, _routerStartTransaction); + } else if (_routerStartTransaction) { + _instrumentEmberRouter(routerService, routerMain, {} as EmberSentryConfig, _routerStartTransaction); } /** @@ -34,10 +49,12 @@ export function setupSentryTest(hooks) { */ this.qunitOnUnhandledRejection = sinon.stub( QUnit, + // @ts-expect-error this is OK QUnit.onUncaughtException ? 'onUncaughtException' : 'onUnhandledRejection', ); - QUnit.onError = function ({ message }) { + // @ts-expect-error this is fine + QUnit.onError = function ({ message }: { message: string }) { errorMessages.push(message.split('Error: ')[1]); return true; }; @@ -52,15 +69,12 @@ export function setupSentryTest(hooks) { /** * Will collect errors when run via testem in cli */ - window.onerror = function (error, ...args) { - errorMessages.push(error.split('Error: ')[1]); - if (this._windowOnError) { - return this._windowOnError(error, ...args); - } + window.onerror = error => { + errorMessages.push(error.toString().split('Error: ')[1]); }; }); - hooks.afterEach(function () { + hooks.afterEach(function (this: SentryTestContext) { this.fetchStub.restore(); this.qunitOnUnhandledRejection.restore(); window.onerror = this._windowOnError; diff --git a/packages/ember/tests/helpers/utils.ts b/packages/ember/tests/helpers/utils.ts new file mode 100644 index 000000000000..3ec336cfa59c --- /dev/null +++ b/packages/ember/tests/helpers/utils.ts @@ -0,0 +1,87 @@ +import type { Event } from '@sentry/types'; + +const defaultAssertOptions = { + method: 'POST', + errorBodyContains: [], +}; + +function getTestSentryErrors(): Event[] { + return window._sentryTestEvents.filter(event => event['type'] !== 'transaction'); +} + +function getTestSentryTransactions(): Event[] { + return window._sentryTestEvents.filter(event => event['type'] === 'transaction'); +} + +export function assertSentryErrorCount(assert: Assert, count: number): void { + assert.equal(getTestSentryErrors().length, count, 'Check correct number of Sentry events were sent'); +} + +export function assertSentryTransactionCount(assert: Assert, count: number): void { + assert.equal(getTestSentryTransactions().length, count, 'Check correct number of Sentry events were sent'); +} + +export function assertSentryErrors( + assert: Assert, + callNumber: number, + options: { + errorBodyContains: string[]; + }, +): void { + const sentryTestEvents = getTestSentryErrors(); + const assertOptions = Object.assign({}, defaultAssertOptions, options); + + const event = sentryTestEvents[callNumber]; + + /** + * Body could be parsed here to check exact properties, but that requires too much implementation specific detail, + * instead this loosely matches on contents to check the correct error is being sent. + */ + assert.ok(assertOptions.errorBodyContains.length, 'Must pass strings to check against error body'); + const errorBody = JSON.stringify(event); + assertOptions.errorBodyContains.forEach(bodyContent => { + assert.ok(errorBody.includes(bodyContent), `Checking that error body includes ${bodyContent}`); + }); +} + +export function assertSentryTransactions( + assert: Assert, + callNumber: number, + options: { + spans: string[]; + transaction: string; + tags: Record; + durationCheck?: (duration: number) => boolean; + }, +): void { + const sentryTestEvents = getTestSentryTransactions(); + const event = sentryTestEvents[callNumber]; + + assert.ok(event); + assert.ok(event.spans); + + const spans = event.spans || []; + + // instead of checking the specific order of runloop spans (which is brittle), + // we check (below) that _any_ runloop spans are added + const filteredSpans = spans + .filter(span => !span.op?.startsWith('ui.ember.runloop.')) + .map(s => { + return `${s.op} | ${s.description}`; + }); + + assert.true( + spans.some(span => span.op?.startsWith('ui.ember.runloop.')), + 'it captures runloop spans', + ); + assert.deepEqual(filteredSpans, options.spans, 'Has correct spans'); + + assert.equal(event.transaction, options.transaction); + assert.equal(event.tags?.fromRoute, options.tags.fromRoute); + assert.equal(event.tags?.toRoute, options.tags.toRoute); + + if (options.durationCheck && event.timestamp && event.start_timestamp) { + const duration = (event.timestamp - event.start_timestamp) * 1000; + assert.ok(options.durationCheck(duration), `duration (${duration}ms) passes duration check`); + } +} diff --git a/packages/ember/tests/test-helper.js b/packages/ember/tests/test-helper.ts similarity index 68% rename from packages/ember/tests/test-helper.js rename to packages/ember/tests/test-helper.ts index 80fbd39cf110..9c04e37d68b5 100644 --- a/packages/ember/tests/test-helper.js +++ b/packages/ember/tests/test-helper.ts @@ -1,10 +1,17 @@ -import * as Sentry from '@sentry/browser'; -import setupSinon from 'ember-sinon-qunit'; -import Application from '../app'; -import config from '../config/environment'; import { setApplication } from '@ember/test-helpers'; -import { start } from 'ember-qunit'; import { isTesting } from '@embroider/macros'; +import * as Sentry from '@sentry/browser'; +import Application from 'dummy/app'; +import config from 'dummy/config/environment'; +import { start } from 'ember-qunit'; +import setupSinon from 'ember-sinon-qunit'; + +declare global { + interface Window { + _sentryTestEvents: Sentry.Event[]; + _sentryPerformanceLoad?: Promise; + } +} Sentry.addGlobalEventProcessor(event => { if (isTesting()) { @@ -21,4 +28,5 @@ setApplication(Application.create(config.APP)); setupSinon(); start(); +// @ts-expect-error TODO: Is this needed ??? QUnit.config.ignoreGlobalErrors = true; diff --git a/packages/ember/tests/unit/instrument-route-performance-test.ts b/packages/ember/tests/unit/instrument-route-performance-test.ts index 548a6e819db2..6ab88a646f23 100644 --- a/packages/ember/tests/unit/instrument-route-performance-test.ts +++ b/packages/ember/tests/unit/instrument-route-performance-test.ts @@ -1,8 +1,9 @@ -import { module, test } from 'qunit'; -import { setupTest } from 'ember-qunit'; import Route from '@ember/routing/route'; import { instrumentRoutePerformance } from '@sentry/ember'; +import { setupTest } from 'ember-qunit'; +import { module, test } from 'qunit'; import sinon from 'sinon'; + import { setupSentryTest } from '../helpers/setup-sentry'; module('Unit | Utility | instrument-route-performance', function (hooks) { @@ -16,19 +17,19 @@ module('Unit | Utility | instrument-route-performance', function (hooks) { const setupController = sinon.spy(); class DummyRoute extends Route { - beforeModel(...args: any[]) { + public beforeModel(...args: unknown[]): unknown { return beforeModel.call(this, ...args); } - model(...args: any[]) { + public model(...args: unknown[]): unknown { return model.call(this, ...args); } - afterModel(...args: any[]) { + public afterModel(...args: unknown[]): unknown { return afterModel.call(this, ...args); } - setupController(...args: any[]) { + public setupController(...args: unknown[]): unknown { return setupController.call(this, ...args); } } @@ -37,7 +38,7 @@ module('Unit | Utility | instrument-route-performance', function (hooks) { this.owner.register('route:dummy', InstrumentedDummyRoute); - const route = this.owner.lookup('route:dummy'); + const route = this.owner.lookup('route:dummy') as DummyRoute; route.beforeModel('foo'); diff --git a/packages/ember/tsconfig.json b/packages/ember/tsconfig.json index c89e04bc98a4..95bb38c78628 100644 --- a/packages/ember/tsconfig.json +++ b/packages/ember/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "target": "es2017", + "target": "es2022", "allowJs": true, "moduleResolution": "node", "allowSyntheticDefaultImports": true, @@ -24,5 +24,5 @@ "*": ["types/*"] } }, - "include": ["app/**/*", "addon/**/*", "types/**/*", "addon-test-support/**/*"] + "include": ["app/**/*", "addon/**/*", "tests/**/*", "types/**/*", "test-support/**/*", "addon-test-support/**/*"] } diff --git a/packages/integrations/src/contextlines.ts b/packages/integrations/src/contextlines.ts new file mode 100644 index 000000000000..3bc483958b42 --- /dev/null +++ b/packages/integrations/src/contextlines.ts @@ -0,0 +1,111 @@ +import type { Event, EventProcessor, Hub, Integration, StackFrame } from '@sentry/types'; +import { addContextToFrame, GLOBAL_OBJ, stripUrlQueryAndFragment } from '@sentry/utils'; + +const WINDOW = GLOBAL_OBJ as typeof GLOBAL_OBJ & Window; + +const DEFAULT_LINES_OF_CONTEXT = 7; + +interface ContextLinesOptions { + /** + * Sets the number of context lines for each frame when loading a file. + * Defaults to 7. + * + * Set to 0 to disable loading and inclusion of source files. + **/ + frameContextLines?: number; +} + +/** + * Collects source context lines around the lines of stackframes pointing to JS embedded in + * the current page's HTML. + * + * This integration DOES NOT work for stack frames pointing to JS files that are loaded by the browser. + * For frames pointing to files, context lines are added during ingestion and symbolication + * by attempting to download the JS files to the Sentry backend. + * + * Use this integration if you have inline JS code in HTML pages that can't be accessed + * by our backend (e.g. due to a login-protected page). + */ +export class ContextLines implements Integration { + /** + * @inheritDoc + */ + public static id: string = 'ContextLines'; + + /** + * @inheritDoc + */ + public name: string; + + public constructor(private readonly _options: ContextLinesOptions = {}) { + this.name = ContextLines.id; + } + + /** + * @inheritDoc + */ + public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void { + addGlobalEventProcessor(event => { + const self = getCurrentHub().getIntegration(ContextLines); + if (!self) { + return event; + } + return this.addSourceContext(event); + }); + } + + /** Processes an event and adds context lines */ + public addSourceContext(event: Event): Event { + const doc = WINDOW.document; + const htmlFilename = WINDOW.location && stripUrlQueryAndFragment(WINDOW.location.href); + if (!doc || !htmlFilename) { + return event; + } + + const exceptions = event.exception && event.exception.values; + if (!exceptions || !exceptions.length) { + return event; + } + + const html = doc.documentElement.innerHTML; + if (!html) { + return event; + } + + const htmlLines = ['', '', ...html.split('\n'), '']; + + exceptions.forEach(exception => { + const stacktrace = exception.stacktrace; + if (stacktrace && stacktrace.frames) { + stacktrace.frames = stacktrace.frames.map(frame => + applySourceContextToFrame( + frame, + htmlLines, + htmlFilename, + this._options.frameContextLines != null ? this._options.frameContextLines : DEFAULT_LINES_OF_CONTEXT, + ), + ); + } + }); + + return event; + } +} + +/** + * Only exported for testing + */ +export function applySourceContextToFrame( + frame: StackFrame, + htmlLines: string[], + htmlFilename: string, + linesOfContext: number, +): StackFrame { + if (frame.filename !== htmlFilename || !frame.lineno || !htmlLines.length) { + return frame; + } + + addContextToFrame(htmlLines, frame, linesOfContext); + + return frame; +} diff --git a/packages/integrations/src/index.ts b/packages/integrations/src/index.ts index 372a730e3a5c..b7dcc1f4716a 100644 --- a/packages/integrations/src/index.ts +++ b/packages/integrations/src/index.ts @@ -9,3 +9,4 @@ export { RewriteFrames } from './rewriteframes'; export { SessionTiming } from './sessiontiming'; export { Transaction } from './transaction'; export { HttpClient } from './httpclient'; +export { ContextLines } from './contextlines'; diff --git a/packages/integrations/test/contextlines.test.ts b/packages/integrations/test/contextlines.test.ts new file mode 100644 index 000000000000..00365d7ab910 --- /dev/null +++ b/packages/integrations/test/contextlines.test.ts @@ -0,0 +1,113 @@ +import type { StackFrame } from '@sentry/types'; + +import { applySourceContextToFrame } from '../src/contextlines'; + +const lines = ['line1', 'line2', 'line3', 'line4', 'line5', 'line6', 'line7', 'line8', 'line9']; +describe('ContextLines', () => { + describe('applySourceContextToFrame', () => { + it.each([ + [ + 5, + { + pre_context: ['line2', 'line3', 'line4'], + context_line: 'line5', + post_context: ['line6', 'line7', 'line8'], + }, + ], + [ + 1, + { + pre_context: [], + context_line: 'line1', + post_context: ['line2', 'line3', 'line4'], + }, + ], + [ + 2, + { + pre_context: ['line1'], + context_line: 'line2', + post_context: ['line3', 'line4', 'line5'], + }, + ], + [ + 9, + { + pre_context: ['line6', 'line7', 'line8'], + context_line: 'line9', + post_context: [], + }, + ], + ])( + 'correctly applies pre, post contexts and context lines for an inline stack frame (lineno %s)', + (lineno, contextLines) => { + const frame: StackFrame = { + lineno, + filename: 'https://mydomain.com/index.html', + }; + + expect(applySourceContextToFrame(frame, lines, 'https://mydomain.com/index.html', 3)).toStrictEqual({ + filename: 'https://mydomain.com/index.html', + lineno, + ...contextLines, + }); + }, + ); + + it('only applies the context line if the range is 0', () => { + const frame: StackFrame = { + lineno: 5, + filename: 'https://mydomain.com/index.html', + }; + + expect(applySourceContextToFrame(frame, lines, 'https://mydomain.com/index.html', 0)).toStrictEqual({ + filename: 'https://mydomain.com/index.html', + lineno: 5, + context_line: 'line5', + pre_context: [], + post_context: [], + }); + }); + + it("no-ops if the frame's line number is out of bounds for the found lines", () => { + const frame: StackFrame = { + lineno: 20, + filename: 'https://mydomain.com/index.html', + }; + + expect(applySourceContextToFrame(frame, lines, 'https://mydomain.com/index.html', 3)).toStrictEqual(frame); + }); + + it("no-ops if the frame's filename is not the html file's name", () => { + const frame: StackFrame = { + filename: '/someScript.js', + }; + + expect(applySourceContextToFrame(frame, lines, 'https://mydomain.com/index.html', 3)).toStrictEqual(frame); + }); + + it("no-ops if the frame doesn't have a line number", () => { + const frame: StackFrame = { + filename: '/index.html', + }; + + expect(applySourceContextToFrame(frame, lines, 'https://mydomain.com/index.html', 0)).toStrictEqual(frame); + }); + + it("no-ops if the frame doesn't have a filename", () => { + const frame: StackFrame = { + lineno: 9, + }; + + expect(applySourceContextToFrame(frame, lines, 'https://mydomain.com/index.html', 0)).toStrictEqual(frame); + }); + + it('no-ops if there are no html lines available', () => { + const frame: StackFrame = { + lineno: 9, + filename: '/index.html', + }; + expect(applySourceContextToFrame(frame, [], 'https://mydomain.com/index.html', 0)).toStrictEqual(frame); + }); + }); +}); diff --git a/packages/nextjs/src/client/index.ts b/packages/nextjs/src/client/index.ts index 665b557e4e7b..0adda732916b 100644 --- a/packages/nextjs/src/client/index.ts +++ b/packages/nextjs/src/client/index.ts @@ -130,38 +130,4 @@ export function withSentryConfig(exportedUserNextConfig: T): T { return exportedUserNextConfig; } -export { - // eslint-disable-next-line deprecation/deprecation - withSentryServerSideGetInitialProps, - wrapGetInitialPropsWithSentry, -} from './wrapGetInitialPropsWithSentry'; - -export { - // eslint-disable-next-line deprecation/deprecation - withSentryServerSideAppGetInitialProps, - wrapAppGetInitialPropsWithSentry, -} from './wrapAppGetInitialPropsWithSentry'; - -export { - // eslint-disable-next-line deprecation/deprecation - withSentryServerSideDocumentGetInitialProps, - wrapDocumentGetInitialPropsWithSentry, -} from './wrapDocumentGetInitialPropsWithSentry'; - -export { - // eslint-disable-next-line deprecation/deprecation - withSentryServerSideErrorGetInitialProps, - wrapErrorGetInitialPropsWithSentry, -} from './wrapErrorGetInitialPropsWithSentry'; - -export { - // eslint-disable-next-line deprecation/deprecation - withSentryGetServerSideProps, - wrapGetServerSidePropsWithSentry, -} from './wrapGetServerSidePropsWithSentry'; - -export { - // eslint-disable-next-line deprecation/deprecation - withSentryGetStaticProps, - wrapGetStaticPropsWithSentry, -} from './wrapGetStaticPropsWithSentry'; +export * from '../common'; diff --git a/packages/nextjs/src/client/wrapAppGetInitialPropsWithSentry.ts b/packages/nextjs/src/client/wrapAppGetInitialPropsWithSentry.ts deleted file mode 100644 index e5f8c40847ff..000000000000 --- a/packages/nextjs/src/client/wrapAppGetInitialPropsWithSentry.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type App from 'next/app'; - -type AppGetInitialProps = (typeof App)['getInitialProps']; - -/** - * A passthrough function in case this function is used on the clientside. We need to make the returned function async - * so we are consistent with the serverside implementation. - */ -export function wrapAppGetInitialPropsWithSentry(origAppGetInitialProps: AppGetInitialProps): AppGetInitialProps { - return new Proxy(origAppGetInitialProps, { - apply: (wrappingTarget, thisArg, args: Parameters) => { - return wrappingTarget.apply(thisArg, args); - }, - }); -} - -/** - * @deprecated Use `wrapAppGetInitialPropsWithSentry` instead. - */ -export const withSentryServerSideAppGetInitialProps = wrapAppGetInitialPropsWithSentry; diff --git a/packages/nextjs/src/client/wrapDocumentGetInitialPropsWithSentry.ts b/packages/nextjs/src/client/wrapDocumentGetInitialPropsWithSentry.ts deleted file mode 100644 index 20669a0af9f6..000000000000 --- a/packages/nextjs/src/client/wrapDocumentGetInitialPropsWithSentry.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type Document from 'next/document'; - -type DocumentGetInitialProps = typeof Document.getInitialProps; - -/** - * A passthrough function in case this function is used on the clientside. We need to make the returned function async - * so we are consistent with the serverside implementation. - */ -export function wrapDocumentGetInitialPropsWithSentry( - origDocumentGetInitialProps: DocumentGetInitialProps, -): DocumentGetInitialProps { - return new Proxy(origDocumentGetInitialProps, { - apply: (wrappingTarget, thisArg, args: Parameters) => { - return wrappingTarget.apply(thisArg, args); - }, - }); -} - -/** - * @deprecated Use `wrapDocumentGetInitialPropsWithSentry` instead. - */ -export const withSentryServerSideDocumentGetInitialProps = wrapDocumentGetInitialPropsWithSentry; diff --git a/packages/nextjs/src/client/wrapErrorGetInitialPropsWithSentry.ts b/packages/nextjs/src/client/wrapErrorGetInitialPropsWithSentry.ts deleted file mode 100644 index ab32a2bf93cc..000000000000 --- a/packages/nextjs/src/client/wrapErrorGetInitialPropsWithSentry.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { NextPageContext } from 'next'; -import type { ErrorProps } from 'next/error'; - -type ErrorGetInitialProps = (context: NextPageContext) => Promise; - -/** - * A passthrough function in case this function is used on the clientside. We need to make the returned function async - * so we are consistent with the serverside implementation. - */ -export function wrapErrorGetInitialPropsWithSentry( - origErrorGetInitialProps: ErrorGetInitialProps, -): ErrorGetInitialProps { - return new Proxy(origErrorGetInitialProps, { - apply: (wrappingTarget, thisArg, args: Parameters) => { - return wrappingTarget.apply(thisArg, args); - }, - }); -} - -/** - * @deprecated Use `wrapErrorGetInitialPropsWithSentry` instead. - */ -export const withSentryServerSideErrorGetInitialProps = wrapErrorGetInitialPropsWithSentry; diff --git a/packages/nextjs/src/client/wrapGetInitialPropsWithSentry.ts b/packages/nextjs/src/client/wrapGetInitialPropsWithSentry.ts deleted file mode 100644 index 37004f04bc6e..000000000000 --- a/packages/nextjs/src/client/wrapGetInitialPropsWithSentry.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { NextPage } from 'next'; - -type GetInitialProps = Required['getInitialProps']; - -/** - * A passthrough function in case this function is used on the clientside. We need to make the returned function async - * so we are consistent with the serverside implementation. - */ -export function wrapGetInitialPropsWithSentry(origGetInitialProps: GetInitialProps): GetInitialProps { - return new Proxy(origGetInitialProps, { - apply: (wrappingTarget, thisArg, args: Parameters) => { - return wrappingTarget.apply(thisArg, args); - }, - }); -} - -/** - * @deprecated Use `wrapGetInitialPropsWithSentry` instead. - */ -export const withSentryServerSideGetInitialProps = wrapGetInitialPropsWithSentry; diff --git a/packages/nextjs/src/client/wrapGetServerSidePropsWithSentry.ts b/packages/nextjs/src/client/wrapGetServerSidePropsWithSentry.ts deleted file mode 100644 index 50450c053a15..000000000000 --- a/packages/nextjs/src/client/wrapGetServerSidePropsWithSentry.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { GetServerSideProps } from 'next'; - -/** - * A passthrough function in case this function is used on the clientside. We need to make the returned function async - * so we are consistent with the serverside implementation. - */ -export function wrapGetServerSidePropsWithSentry(origGetServerSideProps: GetServerSideProps): GetServerSideProps { - return new Proxy(origGetServerSideProps, { - apply: (wrappingTarget, thisArg, args: Parameters) => { - return wrappingTarget.apply(thisArg, args); - }, - }); -} - -/** - * @deprecated Use `withSentryGetServerSideProps` instead. - */ -export const withSentryGetServerSideProps = wrapGetServerSidePropsWithSentry; diff --git a/packages/nextjs/src/client/wrapGetStaticPropsWithSentry.ts b/packages/nextjs/src/client/wrapGetStaticPropsWithSentry.ts deleted file mode 100644 index 3b99737bcf20..000000000000 --- a/packages/nextjs/src/client/wrapGetStaticPropsWithSentry.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { GetStaticProps } from 'next'; - -type Props = { [key: string]: unknown }; - -/** - * A passthrough function in case this function is used on the clientside. We need to make the returned function async - * so we are consistent with the serverside implementation. - */ -export function wrapGetStaticPropsWithSentry(origGetStaticProps: GetStaticProps): GetStaticProps { - return new Proxy(origGetStaticProps, { - apply: (wrappingTarget, thisArg, args: Parameters>) => { - return wrappingTarget.apply(thisArg, args); - }, - }); -} - -/** - * @deprecated Use `wrapGetStaticPropsWithSentry` instead. - */ -export const withSentryGetStaticProps = wrapGetStaticPropsWithSentry; diff --git a/packages/nextjs/src/common/index.ts b/packages/nextjs/src/common/index.ts new file mode 100644 index 000000000000..ccd4a628634e --- /dev/null +++ b/packages/nextjs/src/common/index.ts @@ -0,0 +1,41 @@ +export { + // eslint-disable-next-line deprecation/deprecation + withSentryGetStaticProps, + wrapGetStaticPropsWithSentry, +} from './wrapGetStaticPropsWithSentry'; + +export { + // eslint-disable-next-line deprecation/deprecation + withSentryServerSideGetInitialProps, + wrapGetInitialPropsWithSentry, +} from './wrapGetInitialPropsWithSentry'; + +export { + // eslint-disable-next-line deprecation/deprecation + withSentryServerSideAppGetInitialProps, + wrapAppGetInitialPropsWithSentry, +} from './wrapAppGetInitialPropsWithSentry'; + +export { + // eslint-disable-next-line deprecation/deprecation + withSentryServerSideDocumentGetInitialProps, + wrapDocumentGetInitialPropsWithSentry, +} from './wrapDocumentGetInitialPropsWithSentry'; + +export { + // eslint-disable-next-line deprecation/deprecation + withSentryServerSideErrorGetInitialProps, + wrapErrorGetInitialPropsWithSentry, +} from './wrapErrorGetInitialPropsWithSentry'; + +export { + // eslint-disable-next-line deprecation/deprecation + withSentryGetServerSideProps, + wrapGetServerSidePropsWithSentry, +} from './wrapGetServerSidePropsWithSentry'; + +export { wrapServerComponentWithSentry } from './wrapServerComponentWithSentry'; + +export { wrapApiHandlerWithSentryVercelCrons } from './wrapApiHandlerWithSentryVercelCrons'; + +export { wrapMiddlewareWithSentry } from './wrapMiddlewareWithSentry'; diff --git a/packages/nextjs/src/common/types.ts b/packages/nextjs/src/common/types.ts index 6f1c9e5b2c4b..dc838b214276 100644 --- a/packages/nextjs/src/common/types.ts +++ b/packages/nextjs/src/common/types.ts @@ -1,3 +1,6 @@ +import type { Transaction, WrappedFunction } from '@sentry/types'; +import type { NextApiRequest, NextApiResponse } from 'next'; + export type ServerComponentContext = { componentRoute: string; componentType: string; @@ -6,3 +9,51 @@ export type ServerComponentContext = { }; export type VercelCronsConfig = { path?: string; schedule?: string }[] | undefined; + +// The `NextApiHandler` and `WrappedNextApiHandler` types are the same as the official `NextApiHandler` type, except: +// +// a) The wrapped version returns only promises, because wrapped handlers are always async. +// +// b) Instead of having a return types based on `void` (Next < 12.1.6) or `unknown` (Next 12.1.6+), both the wrapped and +// unwrapped versions of the type have both. This doesn't matter to users, because they exist solely on one side of that +// version divide or the other. For us, though, it's entirely possible to have one version of Next installed in our +// local repo (as a dev dependency) and have another Next version installed in a test app which also has the local SDK +// linked in. +// +// In that case, if those two versions are on either side of the 12.1.6 divide, importing the official `NextApiHandler` +// type here would break the test app's build, because it would set up a situation in which the linked SDK's +// `withSentry` would refer to one version of the type (from the local repo's `node_modules`) while any typed handler in +// the test app would refer to the other version of the type (from the test app's `node_modules`). By using a custom +// version of the type compatible with both the old and new official versions, we can use any Next version we want in a +// test app without worrying about type errors. +// +// c) These have internal SDK flags which the official Next types obviously don't have, one to allow our auto-wrapping +// function, `withSentryAPI`, to pass the parameterized route into `withSentry`, and the other to prevent a manually +// wrapped route from being wrapped again by the auto-wrapper. + +export type NextApiHandler = { + (req: NextApiRequest, res: NextApiResponse): void | Promise | unknown | Promise; + __sentry_route__?: string; + + /** + * A property we set in our integration tests to simulate running an API route on platforms that don't support streaming. + */ + __sentry_test_doesnt_support_streaming__?: true; +}; + +export type WrappedNextApiHandler = { + (req: NextApiRequest, res: NextApiResponse): Promise | Promise; + __sentry_route__?: string; + __sentry_wrapped__?: boolean; +}; + +export type AugmentedNextApiRequest = NextApiRequest & { + __withSentry_applied__?: boolean; +}; + +export type AugmentedNextApiResponse = NextApiResponse & { + __sentryTransaction?: Transaction; +}; + +export type ResponseEndMethod = AugmentedNextApiResponse['end']; +export type WrappedResponseEndMethod = AugmentedNextApiResponse['end'] & WrappedFunction; diff --git a/packages/nextjs/src/edge/utils/edgeWrapperUtils.ts b/packages/nextjs/src/common/utils/edgeWrapperUtils.ts similarity index 93% rename from packages/nextjs/src/edge/utils/edgeWrapperUtils.ts rename to packages/nextjs/src/common/utils/edgeWrapperUtils.ts index 256499c97185..f3023665d106 100644 --- a/packages/nextjs/src/edge/utils/edgeWrapperUtils.ts +++ b/packages/nextjs/src/common/utils/edgeWrapperUtils.ts @@ -1,9 +1,8 @@ -import { captureException, getCurrentHub, hasTracingEnabled, startTransaction } from '@sentry/core'; +import { captureException, flush, getCurrentHub, hasTracingEnabled, startTransaction } from '@sentry/core'; import type { Span } from '@sentry/types'; import { addExceptionMechanism, logger, objectify, tracingContextFromHeaders } from '@sentry/utils'; -import type { EdgeRouteHandler } from '../types'; -import { flush } from './flush'; +import type { EdgeRouteHandler } from '../../edge/types'; /** * Wraps a function on the edge runtime with error and performance monitoring. diff --git a/packages/nextjs/src/server/utils/isBuild.ts b/packages/nextjs/src/common/utils/isBuild.ts similarity index 100% rename from packages/nextjs/src/server/utils/isBuild.ts rename to packages/nextjs/src/common/utils/isBuild.ts diff --git a/packages/nextjs/src/server/utils/platformSupportsStreaming.ts b/packages/nextjs/src/common/utils/platformSupportsStreaming.ts similarity index 100% rename from packages/nextjs/src/server/utils/platformSupportsStreaming.ts rename to packages/nextjs/src/common/utils/platformSupportsStreaming.ts diff --git a/packages/nextjs/src/server/utils/responseEnd.ts b/packages/nextjs/src/common/utils/responseEnd.ts similarity index 80% rename from packages/nextjs/src/server/utils/responseEnd.ts rename to packages/nextjs/src/common/utils/responseEnd.ts index ee2d9f803d3b..4cc31de89fb5 100644 --- a/packages/nextjs/src/server/utils/responseEnd.ts +++ b/packages/nextjs/src/common/utils/responseEnd.ts @@ -1,4 +1,4 @@ -import { flush } from '@sentry/node'; +import { flush } from '@sentry/core'; import type { Transaction } from '@sentry/types'; import { fill, logger } from '@sentry/utils'; import type { ServerResponse } from 'http'; @@ -41,18 +41,7 @@ export function autoEndTransactionOnResponseEnd(transaction: Transaction, res: S export async function finishTransaction(transaction: Transaction | undefined, res: ServerResponse): Promise { if (transaction) { transaction.setHttpStatus(res.statusCode); - - // If any open spans are set to finish when the response ends, it sets up a race condition between their `finish` - // calls and the transaction's `finish` call - and any spans which lose the race will get dropped from the - // transaction. To prevent this, push `transaction.finish` to the next event loop so that it's guaranteed to lose - // the race, and wait for it to be done before flushing events. - const transactionFinished: Promise = new Promise(resolve => { - setImmediate(() => { - transaction.finish(); - resolve(); - }); - }); - await transactionFinished; + transaction.finish(); } } diff --git a/packages/nextjs/src/server/utils/wrapperUtils.ts b/packages/nextjs/src/common/utils/wrapperUtils.ts similarity index 100% rename from packages/nextjs/src/server/utils/wrapperUtils.ts rename to packages/nextjs/src/common/utils/wrapperUtils.ts diff --git a/packages/nextjs/src/server/wrapApiHandlerWithSentry.ts b/packages/nextjs/src/common/wrapApiHandlerWithSentry.ts similarity index 98% rename from packages/nextjs/src/server/wrapApiHandlerWithSentry.ts rename to packages/nextjs/src/common/wrapApiHandlerWithSentry.ts index 524e29fa0ca0..b4af7d47893e 100644 --- a/packages/nextjs/src/server/wrapApiHandlerWithSentry.ts +++ b/packages/nextjs/src/common/wrapApiHandlerWithSentry.ts @@ -1,5 +1,10 @@ -import { getCurrentHub, hasTracingEnabled, runWithAsyncContext } from '@sentry/core'; -import { captureException, startTransaction } from '@sentry/node'; +import { + captureException, + getCurrentHub, + hasTracingEnabled, + runWithAsyncContext, + startTransaction, +} from '@sentry/core'; import type { Transaction } from '@sentry/types'; import { addExceptionMechanism, diff --git a/packages/nextjs/src/server/wrapAppGetInitialPropsWithSentry.ts b/packages/nextjs/src/common/wrapAppGetInitialPropsWithSentry.ts similarity index 97% rename from packages/nextjs/src/server/wrapAppGetInitialPropsWithSentry.ts rename to packages/nextjs/src/common/wrapAppGetInitialPropsWithSentry.ts index e1ecf50dad54..c39b903f717b 100644 --- a/packages/nextjs/src/server/wrapAppGetInitialPropsWithSentry.ts +++ b/packages/nextjs/src/common/wrapAppGetInitialPropsWithSentry.ts @@ -1,5 +1,4 @@ -import { hasTracingEnabled } from '@sentry/core'; -import { getCurrentHub } from '@sentry/node'; +import { getCurrentHub, hasTracingEnabled } from '@sentry/core'; import { dynamicSamplingContextToSentryBaggageHeader } from '@sentry/utils'; import type App from 'next/app'; diff --git a/packages/nextjs/src/server/wrapDocumentGetInitialPropsWithSentry.ts b/packages/nextjs/src/common/wrapDocumentGetInitialPropsWithSentry.ts similarity index 95% rename from packages/nextjs/src/server/wrapDocumentGetInitialPropsWithSentry.ts rename to packages/nextjs/src/common/wrapDocumentGetInitialPropsWithSentry.ts index 1d821d86dcda..54b8c614f799 100644 --- a/packages/nextjs/src/server/wrapDocumentGetInitialPropsWithSentry.ts +++ b/packages/nextjs/src/common/wrapDocumentGetInitialPropsWithSentry.ts @@ -1,5 +1,4 @@ -import { hasTracingEnabled } from '@sentry/core'; -import { getCurrentHub } from '@sentry/node'; +import { getCurrentHub, hasTracingEnabled } from '@sentry/core'; import type Document from 'next/document'; import { isBuild } from './utils/isBuild'; diff --git a/packages/nextjs/src/server/wrapErrorGetInitialPropsWithSentry.ts b/packages/nextjs/src/common/wrapErrorGetInitialPropsWithSentry.ts similarity index 96% rename from packages/nextjs/src/server/wrapErrorGetInitialPropsWithSentry.ts rename to packages/nextjs/src/common/wrapErrorGetInitialPropsWithSentry.ts index b99b04524d46..9ebf810e5ee0 100644 --- a/packages/nextjs/src/server/wrapErrorGetInitialPropsWithSentry.ts +++ b/packages/nextjs/src/common/wrapErrorGetInitialPropsWithSentry.ts @@ -1,5 +1,4 @@ -import { hasTracingEnabled } from '@sentry/core'; -import { getCurrentHub } from '@sentry/node'; +import { getCurrentHub, hasTracingEnabled } from '@sentry/core'; import { dynamicSamplingContextToSentryBaggageHeader } from '@sentry/utils'; import type { NextPageContext } from 'next'; import type { ErrorProps } from 'next/error'; diff --git a/packages/nextjs/src/server/wrapGetInitialPropsWithSentry.ts b/packages/nextjs/src/common/wrapGetInitialPropsWithSentry.ts similarity index 96% rename from packages/nextjs/src/server/wrapGetInitialPropsWithSentry.ts rename to packages/nextjs/src/common/wrapGetInitialPropsWithSentry.ts index d1613a80d2c9..601105bab7be 100644 --- a/packages/nextjs/src/server/wrapGetInitialPropsWithSentry.ts +++ b/packages/nextjs/src/common/wrapGetInitialPropsWithSentry.ts @@ -1,5 +1,4 @@ -import { hasTracingEnabled } from '@sentry/core'; -import { getCurrentHub } from '@sentry/node'; +import { getCurrentHub, hasTracingEnabled } from '@sentry/core'; import { dynamicSamplingContextToSentryBaggageHeader } from '@sentry/utils'; import type { NextPage } from 'next'; diff --git a/packages/nextjs/src/server/wrapGetServerSidePropsWithSentry.ts b/packages/nextjs/src/common/wrapGetServerSidePropsWithSentry.ts similarity index 96% rename from packages/nextjs/src/server/wrapGetServerSidePropsWithSentry.ts rename to packages/nextjs/src/common/wrapGetServerSidePropsWithSentry.ts index f37068bae206..f0a0517960fc 100644 --- a/packages/nextjs/src/server/wrapGetServerSidePropsWithSentry.ts +++ b/packages/nextjs/src/common/wrapGetServerSidePropsWithSentry.ts @@ -1,5 +1,4 @@ -import { hasTracingEnabled } from '@sentry/core'; -import { getCurrentHub } from '@sentry/node'; +import { getCurrentHub, hasTracingEnabled } from '@sentry/core'; import { dynamicSamplingContextToSentryBaggageHeader } from '@sentry/utils'; import type { GetServerSideProps } from 'next'; diff --git a/packages/nextjs/src/server/wrapGetStaticPropsWithSentry.ts b/packages/nextjs/src/common/wrapGetStaticPropsWithSentry.ts similarity index 93% rename from packages/nextjs/src/server/wrapGetStaticPropsWithSentry.ts rename to packages/nextjs/src/common/wrapGetStaticPropsWithSentry.ts index 78f910dfb0e4..e65d0ba8f90a 100644 --- a/packages/nextjs/src/server/wrapGetStaticPropsWithSentry.ts +++ b/packages/nextjs/src/common/wrapGetStaticPropsWithSentry.ts @@ -1,5 +1,4 @@ -import { hasTracingEnabled } from '@sentry/core'; -import { getCurrentHub } from '@sentry/node'; +import { getCurrentHub, hasTracingEnabled } from '@sentry/core'; import type { GetStaticProps } from 'next'; import { isBuild } from './utils/isBuild'; diff --git a/packages/nextjs/src/edge/wrapMiddlewareWithSentry.ts b/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts similarity index 92% rename from packages/nextjs/src/edge/wrapMiddlewareWithSentry.ts rename to packages/nextjs/src/common/wrapMiddlewareWithSentry.ts index 831a50eb8629..66cbbb046300 100644 --- a/packages/nextjs/src/edge/wrapMiddlewareWithSentry.ts +++ b/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts @@ -1,4 +1,4 @@ -import type { EdgeRouteHandler } from './types'; +import type { EdgeRouteHandler } from '../edge/types'; import { withEdgeWrapping } from './utils/edgeWrapperUtils'; /** diff --git a/packages/nextjs/src/server/wrapServerComponentWithSentry.ts b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts similarity index 100% rename from packages/nextjs/src/server/wrapServerComponentWithSentry.ts rename to packages/nextjs/src/common/wrapServerComponentWithSentry.ts diff --git a/packages/nextjs/src/config/templates/apiWrapperTemplate.ts b/packages/nextjs/src/config/templates/apiWrapperTemplate.ts index 0eccf3024a76..28d5e4efc806 100644 --- a/packages/nextjs/src/config/templates/apiWrapperTemplate.ts +++ b/packages/nextjs/src/config/templates/apiWrapperTemplate.ts @@ -13,10 +13,7 @@ import * as origModule from '__SENTRY_WRAPPING_TARGET_FILE__'; import * as Sentry from '@sentry/nextjs'; import type { PageConfig } from 'next'; -import type { VercelCronsConfig } from '../../common/types'; -// We import this from `wrappers` rather than directly from `next` because our version can work simultaneously with -// multiple versions of next. See note in `wrappers/types` for more. -import type { NextApiHandler } from '../../server/types'; +import type { NextApiHandler, VercelCronsConfig } from '../../common/types'; type NextApiModule = ( | { diff --git a/packages/nextjs/src/edge/index.ts b/packages/nextjs/src/edge/index.ts index 70cba9c356e6..6c2967d30f9b 100644 --- a/packages/nextjs/src/edge/index.ts +++ b/packages/nextjs/src/edge/index.ts @@ -1,12 +1,6 @@ -import { getCurrentHub, getIntegrationsToSetup, initAndBind, Integrations as CoreIntegrations } from '@sentry/core'; +import { 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 { createStackParser, GLOBAL_OBJ, nodeStackLineParser, stackParserFromStackParserOptions } from '@sentry/utils'; import { getVercelEnv } from '../common/getVercelEnv'; import { setAsyncLocalStorageAsyncContextStrategy } from './asyncLocalStorageAsyncContextStrategy'; @@ -103,32 +97,6 @@ export function getSentryRelease(fallback?: string): string | undefined { ); } -/** - * 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 { - const client = getCurrentHub().getClient(); - if (client) { - return client.close(timeout); - } - __DEBUG_BUILD__ && logger.warn('Cannot flush events and disable SDK. 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(); -} - /** * Just a passthrough in case this is imported from the client. */ @@ -136,18 +104,14 @@ export function withSentryConfig(exportedUserNextConfig: T): T { return exportedUserNextConfig; } -export { flush } from './utils/flush'; - export * from '@sentry/core'; +// eslint-disable-next-line import/export +export * from '../common'; + export { - // eslint-disable-next-line deprecation/deprecation + // eslint-disable-next-line deprecation/deprecation, import/export withSentryAPI, + // eslint-disable-next-line import/export wrapApiHandlerWithSentry, } from './wrapApiHandlerWithSentry'; - -export { wrapApiHandlerWithSentryVercelCrons } from '../common/wrapApiHandlerWithSentryVercelCrons'; - -export { wrapMiddlewareWithSentry } from './wrapMiddlewareWithSentry'; - -export { wrapServerComponentWithSentry } from './wrapServerComponentWithSentry'; diff --git a/packages/nextjs/src/edge/utils/flush.ts b/packages/nextjs/src/edge/utils/flush.ts deleted file mode 100644 index 5daa52936391..000000000000 --- a/packages/nextjs/src/edge/utils/flush.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { getCurrentHub } from '@sentry/core'; -import type { Client } from '@sentry/types'; -import { logger } from '@sentry/utils'; - -/** - * 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 { - const client = getCurrentHub().getClient(); - if (client) { - return client.flush(timeout); - } - __DEBUG_BUILD__ && logger.warn('Cannot flush events. No client defined.'); - return Promise.resolve(false); -} diff --git a/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts b/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts index f903d77f46c4..46691b3cdce5 100644 --- a/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts +++ b/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts @@ -1,7 +1,7 @@ import { getCurrentHub } from '@sentry/core'; +import { withEdgeWrapping } from '../common/utils/edgeWrapperUtils'; import type { EdgeRouteHandler } from './types'; -import { withEdgeWrapping } from './utils/edgeWrapperUtils'; /** * Wraps a Next.js edge route handler with Sentry error and performance instrumentation. diff --git a/packages/nextjs/src/edge/wrapServerComponentWithSentry.ts b/packages/nextjs/src/edge/wrapServerComponentWithSentry.ts deleted file mode 100644 index 349207e7b039..000000000000 --- a/packages/nextjs/src/edge/wrapServerComponentWithSentry.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { captureException } from '@sentry/core'; - -import type { ServerComponentContext } from '../common/types'; - -/** - * Wraps an `app` directory server component with Sentry error instrumentation. - */ -export function wrapServerComponentWithSentry any>( - appDirComponent: F, - _context: ServerComponentContext, -): F { - // Even though users may define server components as async functions, for the client bundles - // Next.js will turn them into synchronous functions and it will transform any`await`s into instances of the`use` - // hook. 🤯 - return new Proxy(appDirComponent, { - apply: (originalFunction, thisArg, args) => { - let maybePromiseResult; - - try { - maybePromiseResult = originalFunction.apply(thisArg, args); - } catch (e) { - captureException(e); - throw e; - } - - if (typeof maybePromiseResult === 'object' && maybePromiseResult !== null && 'then' in maybePromiseResult) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - return maybePromiseResult.then(null, (e: Error) => { - captureException(e); - throw e; - }); - } else { - return maybePromiseResult; - } - }, - }); -} diff --git a/packages/nextjs/src/index.types.ts b/packages/nextjs/src/index.types.ts index 6fcf7436399b..5fbcf683ec38 100644 --- a/packages/nextjs/src/index.types.ts +++ b/packages/nextjs/src/index.types.ts @@ -27,9 +27,6 @@ export declare const Integrations: typeof clientSdk.Integrations & export declare const defaultIntegrations: Integration[]; export declare const defaultStackParser: StackParser; -export declare function close(timeout?: number | undefined): PromiseLike; -export declare function flush(timeout?: number | undefined): PromiseLike; -export declare function lastEventId(): string | undefined; export declare function getSentryRelease(fallback?: string): string | undefined; export declare const ErrorBoundary: typeof clientSdk.ErrorBoundary; diff --git a/packages/nextjs/src/server/index.ts b/packages/nextjs/src/server/index.ts index d92e406cda3f..0e43291d019d 100644 --- a/packages/nextjs/src/server/index.ts +++ b/packages/nextjs/src/server/index.ts @@ -10,7 +10,7 @@ import * as path from 'path'; import { devErrorSymbolicationEventProcessor } from '../common/devErrorSymbolicationEventProcessor'; import { getVercelEnv } from '../common/getVercelEnv'; import { buildMetadata } from '../common/metadata'; -import { isBuild } from './utils/isBuild'; +import { isBuild } from '../common/utils/isBuild'; export * from '@sentry/node'; export { captureUnderscoreErrorException } from '../common/_error'; @@ -157,41 +157,7 @@ const deprecatedIsBuild = (): boolean => isBuild(); // eslint-disable-next-line deprecation/deprecation export { deprecatedIsBuild as isBuild }; -export { wrapApiHandlerWithSentryVercelCrons } from '../common/wrapApiHandlerWithSentryVercelCrons'; - -export { - // eslint-disable-next-line deprecation/deprecation - withSentryGetStaticProps, - wrapGetStaticPropsWithSentry, -} from './wrapGetStaticPropsWithSentry'; - -export { - // eslint-disable-next-line deprecation/deprecation - withSentryServerSideGetInitialProps, - wrapGetInitialPropsWithSentry, -} from './wrapGetInitialPropsWithSentry'; - -export { - // eslint-disable-next-line deprecation/deprecation - withSentryServerSideAppGetInitialProps, - wrapAppGetInitialPropsWithSentry, -} from './wrapAppGetInitialPropsWithSentry'; -export { - // eslint-disable-next-line deprecation/deprecation - withSentryServerSideDocumentGetInitialProps, - wrapDocumentGetInitialPropsWithSentry, -} from './wrapDocumentGetInitialPropsWithSentry'; -export { - // eslint-disable-next-line deprecation/deprecation - withSentryServerSideErrorGetInitialProps, - wrapErrorGetInitialPropsWithSentry, -} from './wrapErrorGetInitialPropsWithSentry'; - -export { - // eslint-disable-next-line deprecation/deprecation - withSentryGetServerSideProps, - wrapGetServerSidePropsWithSentry, -} from './wrapGetServerSidePropsWithSentry'; +export * from '../common'; export { // eslint-disable-next-line deprecation/deprecation @@ -199,6 +165,4 @@ export { // eslint-disable-next-line deprecation/deprecation withSentryAPI, wrapApiHandlerWithSentry, -} from './wrapApiHandlerWithSentry'; - -export { wrapServerComponentWithSentry } from './wrapServerComponentWithSentry'; +} from '../common/wrapApiHandlerWithSentry'; diff --git a/packages/nextjs/src/server/types.ts b/packages/nextjs/src/server/types.ts deleted file mode 100644 index a411c4ea62cf..000000000000 --- a/packages/nextjs/src/server/types.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type { Transaction, WrappedFunction } from '@sentry/types'; -import type { NextApiRequest, NextApiResponse } from 'next'; - -// The `NextApiHandler` and `WrappedNextApiHandler` types are the same as the official `NextApiHandler` type, except: -// -// a) The wrapped version returns only promises, because wrapped handlers are always async. -// -// b) Instead of having a return types based on `void` (Next < 12.1.6) or `unknown` (Next 12.1.6+), both the wrapped and -// unwrapped versions of the type have both. This doesn't matter to users, because they exist solely on one side of that -// version divide or the other. For us, though, it's entirely possible to have one version of Next installed in our -// local repo (as a dev dependency) and have another Next version installed in a test app which also has the local SDK -// linked in. -// -// In that case, if those two versions are on either side of the 12.1.6 divide, importing the official `NextApiHandler` -// type here would break the test app's build, because it would set up a situation in which the linked SDK's -// `withSentry` would refer to one version of the type (from the local repo's `node_modules`) while any typed handler in -// the test app would refer to the other version of the type (from the test app's `node_modules`). By using a custom -// version of the type compatible with both the old and new official versions, we can use any Next version we want in a -// test app without worrying about type errors. -// -// c) These have internal SDK flags which the official Next types obviously don't have, one to allow our auto-wrapping -// function, `withSentryAPI`, to pass the parameterized route into `withSentry`, and the other to prevent a manually -// wrapped route from being wrapped again by the auto-wrapper. - -export type NextApiHandler = { - (req: NextApiRequest, res: NextApiResponse): void | Promise | unknown | Promise; - __sentry_route__?: string; - - /** - * A property we set in our integration tests to simulate running an API route on platforms that don't support streaming. - */ - __sentry_test_doesnt_support_streaming__?: true; -}; - -export type WrappedNextApiHandler = { - (req: NextApiRequest, res: NextApiResponse): Promise | Promise; - __sentry_route__?: string; - __sentry_wrapped__?: boolean; -}; - -export type AugmentedNextApiRequest = NextApiRequest & { - __withSentry_applied__?: boolean; -}; - -export type AugmentedNextApiResponse = NextApiResponse & { - __sentryTransaction?: Transaction; -}; - -export type ResponseEndMethod = AugmentedNextApiResponse['end']; -export type WrappedResponseEndMethod = AugmentedNextApiResponse['end'] & WrappedFunction; diff --git a/packages/nextjs/test/config/withSentry.test.ts b/packages/nextjs/test/config/withSentry.test.ts index 92315374836b..c7862e1473df 100644 --- a/packages/nextjs/test/config/withSentry.test.ts +++ b/packages/nextjs/test/config/withSentry.test.ts @@ -1,11 +1,10 @@ -import * as hub from '@sentry/core'; +import * as SentryCore from '@sentry/core'; import { addTracingExtensions } from '@sentry/core'; -import * as Sentry from '@sentry/node'; import type { Client, ClientOptions } from '@sentry/types'; import type { NextApiRequest, NextApiResponse } from 'next'; +import type { AugmentedNextApiResponse, NextApiHandler } from '../../src/common/types'; import { withSentry } from '../../src/server'; -import type { AugmentedNextApiResponse, NextApiHandler } from '../../src/server/types'; // The wrap* functions require the hub to have tracing extensions. This is normally called by the NodeClient // constructor but the client isn't used in these tests. @@ -41,7 +40,7 @@ async function callWrappedHandler(wrappedHandler: NextApiHandler, req: NextApiRe } } -const startTransactionSpy = jest.spyOn(Sentry, 'startTransaction'); +const startTransactionSpy = jest.spyOn(SentryCore, 'startTransaction'); describe('withSentry', () => { let req: NextApiRequest, res: NextApiResponse; @@ -71,7 +70,7 @@ describe('withSentry', () => { describe('tracing', () => { it('starts a transaction and sets metadata when tracing is enabled', async () => { - jest.spyOn(hub.Hub.prototype, 'getClient').mockReturnValueOnce({ + jest.spyOn(SentryCore.Hub.prototype, 'getClient').mockReturnValueOnce({ getOptions: () => ({ tracesSampleRate: 1, instrumenter: 'sentry' } as ClientOptions), } as Client); diff --git a/packages/nextjs/test/config/wrappers.test.ts b/packages/nextjs/test/config/wrappers.test.ts index 9195154991be..44528ec17cc1 100644 --- a/packages/nextjs/test/config/wrappers.test.ts +++ b/packages/nextjs/test/config/wrappers.test.ts @@ -1,9 +1,8 @@ import * as SentryCore from '@sentry/core'; import { addTracingExtensions } from '@sentry/core'; -import * as SentryNode from '@sentry/node'; import type { IncomingMessage, ServerResponse } from 'http'; -import { wrapGetInitialPropsWithSentry, wrapGetServerSidePropsWithSentry } from '../../src/server'; +import { wrapGetInitialPropsWithSentry, wrapGetServerSidePropsWithSentry } from '../../src/common'; const startTransactionSpy = jest.spyOn(SentryCore, 'startTransaction'); const originalGetCurrentHub = jest.requireActual('@sentry/node').getCurrentHub; @@ -23,7 +22,7 @@ describe('data-fetching function wrappers', () => { res = { end: jest.fn() } as unknown as ServerResponse; jest.spyOn(SentryCore, 'hasTracingEnabled').mockReturnValue(true); - jest.spyOn(SentryNode, 'getCurrentHub').mockImplementation(() => { + jest.spyOn(SentryCore, 'getCurrentHub').mockImplementation(() => { const hub = originalGetCurrentHub(); hub.getClient = () => diff --git a/packages/nextjs/test/edge/edgeWrapperUtils.test.ts b/packages/nextjs/test/edge/edgeWrapperUtils.test.ts index cdc7cc4986e2..3dd963077a00 100644 --- a/packages/nextjs/test/edge/edgeWrapperUtils.test.ts +++ b/packages/nextjs/test/edge/edgeWrapperUtils.test.ts @@ -1,7 +1,7 @@ import * as coreSdk from '@sentry/core'; import { addTracingExtensions } from '@sentry/core'; -import { withEdgeWrapping } from '../../src/edge/utils/edgeWrapperUtils'; +import { withEdgeWrapping } from '../../src/common/utils/edgeWrapperUtils'; // The wrap* functions require the hub to have tracing extensions. This is normally called by the EdgeClient // constructor but the client isn't used in these tests. diff --git a/packages/node/src/handlers.ts b/packages/node/src/handlers.ts index 1d1ef3bed507..5d28be6d3c21 100644 --- a/packages/node/src/handlers.ts +++ b/packages/node/src/handlers.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { captureException, + flush, getCurrentHub, hasTracingEnabled, runWithAsyncContext, @@ -25,7 +26,7 @@ import type { NodeClient } from './client'; import { extractRequestData } from './requestdata'; // TODO (v8 / XXX) Remove this import import type { ParseRequestOptions } from './requestDataDeprecated'; -import { flush, isAutoSessionTrackingEnabled } from './sdk'; +import { isAutoSessionTrackingEnabled } from './sdk'; /** * Express-compatible tracing handler. diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 1dd778e21199..0031a587c602 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -29,13 +29,16 @@ export { captureException, captureEvent, captureMessage, + close, configureScope, createTransport, extractTraceparentData, + flush, getActiveTransaction, getHubFromCarrier, getCurrentHub, Hub, + lastEventId, makeMain, runWithAsyncContext, Scope, @@ -57,7 +60,7 @@ export { autoDiscoverNodePerformanceMonitoringIntegrations } from './tracing'; export { NodeClient } from './client'; export { makeNodeTransport } from './transports'; -export { defaultIntegrations, init, defaultStackParser, lastEventId, flush, close, getSentryRelease } from './sdk'; +export { defaultIntegrations, init, defaultStackParser, getSentryRelease } from './sdk'; export { addRequestDataToEvent, DEFAULT_USER_INCLUDES, extractRequestData } from './requestdata'; export { deepReadDirSync } from './utils'; export { getModuleFromFilename } from './module'; diff --git a/packages/node/src/sdk.ts b/packages/node/src/sdk.ts index 7287d1938346..20e8160b7985 100644 --- a/packages/node/src/sdk.ts +++ b/packages/node/src/sdk.ts @@ -10,7 +10,6 @@ import type { SessionStatus, StackParser } from '@sentry/types'; import { createStackParser, GLOBAL_OBJ, - logger, nodeStackLineParser, stackParserFromStackParserOptions, tracingContextFromHeaders, @@ -177,49 +176,6 @@ export function init(options: NodeOptions = {}): void { updateScopeFromEnvVariables(); } -/** - * This is the getter for lastEventId. - * - * @returns The last event id of a captured event. - */ -export function lastEventId(): string | undefined { - return getCurrentHub().lastEventId(); -} - -/** - * 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 { - const client = getCurrentHub().getClient(); - if (client) { - return client.flush(timeout); - } - __DEBUG_BUILD__ && logger.warn('Cannot flush events. No client defined.'); - return Promise.resolve(false); -} - -/** - * 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 { - const client = getCurrentHub().getClient(); - if (client) { - return client.close(timeout); - } - __DEBUG_BUILD__ && logger.warn('Cannot flush events and disable SDK. No client defined.'); - return Promise.resolve(false); -} - /** * Function that takes an instance of NodeClient and checks if autoSessionTracking option is enabled for that client */ diff --git a/packages/node/test/handlers.test.ts b/packages/node/test/handlers.test.ts index d464342fe396..e31383118c82 100644 --- a/packages/node/test/handlers.test.ts +++ b/packages/node/test/handlers.test.ts @@ -7,7 +7,6 @@ import * as http from 'http'; import { NodeClient } from '../src/client'; import { errorHandler, requestHandler, tracingHandler } from '../src/handlers'; -import * as SDK from '../src/sdk'; import { getDefaultNodeClientOptions } from './helper/node-client-options'; function mockAsyncContextStrategy(getHub: () => Hub): void { @@ -128,7 +127,7 @@ describe('requestHandler', () => { }); it('patches `res.end` when `flushTimeout` is specified', done => { - const flush = jest.spyOn(SDK, 'flush').mockResolvedValue(true); + const flush = jest.spyOn(sentryCore, 'flush').mockResolvedValue(true); const sentryRequestMiddleware = requestHandler({ flushTimeout: 1337 }); sentryRequestMiddleware(req, res, next); @@ -142,7 +141,7 @@ describe('requestHandler', () => { }); it('prevents errors thrown during `flush` from breaking the response', done => { - jest.spyOn(SDK, 'flush').mockRejectedValue(new SentryError('HTTP Error (429)')); + jest.spyOn(sentryCore, 'flush').mockRejectedValue(new SentryError('HTTP Error (429)')); const sentryRequestMiddleware = requestHandler({ flushTimeout: 1337 }); sentryRequestMiddleware(req, res, next); diff --git a/packages/replay/src/eventBuffer/EventBufferArray.ts b/packages/replay/src/eventBuffer/EventBufferArray.ts index c9915e7c8b05..2eb760409d9f 100644 --- a/packages/replay/src/eventBuffer/EventBufferArray.ts +++ b/packages/replay/src/eventBuffer/EventBufferArray.ts @@ -10,11 +10,16 @@ import { EventBufferSizeExceededError } from './error'; export class EventBufferArray implements EventBuffer { /** All the events that are buffered to be sent. */ public events: RecordingEvent[]; + + /** @inheritdoc */ + public hasCheckout: boolean; + private _totalSize: number; public constructor() { this.events = []; this._totalSize = 0; + this.hasCheckout = false; } /** @inheritdoc */ @@ -59,6 +64,7 @@ export class EventBufferArray implements EventBuffer { public clear(): void { this.events = []; this._totalSize = 0; + this.hasCheckout = false; } /** @inheritdoc */ diff --git a/packages/replay/src/eventBuffer/EventBufferCompressionWorker.ts b/packages/replay/src/eventBuffer/EventBufferCompressionWorker.ts index 8c40c5d289cf..90e1eeba8f11 100644 --- a/packages/replay/src/eventBuffer/EventBufferCompressionWorker.ts +++ b/packages/replay/src/eventBuffer/EventBufferCompressionWorker.ts @@ -11,6 +11,9 @@ import { WorkerHandler } from './WorkerHandler'; * Exported only for testing. */ export class EventBufferCompressionWorker implements EventBuffer { + /** @inheritdoc */ + public hasCheckout: boolean; + private _worker: WorkerHandler; private _earliestTimestamp: number | null; private _totalSize; @@ -19,6 +22,7 @@ export class EventBufferCompressionWorker implements EventBuffer { this._worker = new WorkerHandler(worker); this._earliestTimestamp = null; this._totalSize = 0; + this.hasCheckout = false; } /** @inheritdoc */ @@ -78,6 +82,8 @@ export class EventBufferCompressionWorker implements EventBuffer { public clear(): void { this._earliestTimestamp = null; this._totalSize = 0; + this.hasCheckout = false; + // We do not wait on this, as we assume the order of messages is consistent for the worker void this._worker.postMessage('clear'); } diff --git a/packages/replay/src/eventBuffer/EventBufferProxy.ts b/packages/replay/src/eventBuffer/EventBufferProxy.ts index 2b3c2329f876..0b5a6bdfed11 100644 --- a/packages/replay/src/eventBuffer/EventBufferProxy.ts +++ b/packages/replay/src/eventBuffer/EventBufferProxy.ts @@ -35,6 +35,15 @@ export class EventBufferProxy implements EventBuffer { return this._used.hasEvents; } + /** @inheritdoc */ + public get hasCheckout(): boolean { + return this._used.hasCheckout; + } + /** @inheritdoc */ + public set hasCheckout(value: boolean) { + this._used.hasCheckout = value; + } + /** @inheritDoc */ public destroy(): void { this._fallback.destroy(); diff --git a/packages/replay/src/replay.ts b/packages/replay/src/replay.ts index 18352fbf139b..dd82e26f3d57 100644 --- a/packages/replay/src/replay.ts +++ b/packages/replay/src/replay.ts @@ -47,7 +47,7 @@ import { debounce } from './util/debounce'; import { getHandleRecordingEmit } from './util/handleRecordingEmit'; import { isExpired } from './util/isExpired'; import { isSessionExpired } from './util/isSessionExpired'; -import { logInfo } from './util/log'; +import { logInfo, logInfoNextTick } from './util/log'; import { sendReplay } from './util/sendReplay'; import type { SKIPPED } from './util/throttle'; import { throttle, THROTTLED } from './util/throttle'; @@ -250,7 +250,10 @@ export class ReplayContainer implements ReplayContainerInterface { this.recordingMode = 'buffer'; } - logInfo(`[Replay] Starting replay in ${this.recordingMode} mode`, this._options._experiments.traceInternals); + logInfoNextTick( + `[Replay] Starting replay in ${this.recordingMode} mode`, + this._options._experiments.traceInternals, + ); this._initializeRecording(); } @@ -271,7 +274,7 @@ export class ReplayContainer implements ReplayContainerInterface { throw new Error('Replay buffering is in progress, call `flush()` to save the replay'); } - logInfo('[Replay] Starting replay in session mode', this._options._experiments.traceInternals); + logInfoNextTick('[Replay] Starting replay in session mode', this._options._experiments.traceInternals); const previousSessionId = this.session && this.session.id; @@ -300,7 +303,7 @@ export class ReplayContainer implements ReplayContainerInterface { throw new Error('Replay recording is already in progress'); } - logInfo('[Replay] Starting replay in buffer mode', this._options._experiments.traceInternals); + logInfoNextTick('[Replay] Starting replay in buffer mode', this._options._experiments.traceInternals); const previousSessionId = this.session && this.session.id; @@ -1130,6 +1133,9 @@ export class ReplayContainer implements ReplayContainerInterface { const now = Date.now(); const duration = now - start; + // A flush is about to happen, cancel any queued flushes + this._debouncedFlush.cancel(); + // If session is too short, or too long (allow some wiggle room over maxSessionLife), do not send it // This _should_ not happen, but it may happen if flush is triggered due to a page activity change or similar const tooShort = duration < this._options.minReplayDuration; @@ -1139,6 +1145,7 @@ export class ReplayContainer implements ReplayContainerInterface { `[Replay] Session duration (${Math.floor(duration / 1000)}s) is too ${ tooShort ? 'short' : 'long' }, not sending replay.`, + this._options._experiments.traceInternals, ); if (tooShort) { @@ -1147,8 +1154,11 @@ export class ReplayContainer implements ReplayContainerInterface { return; } - // A flush is about to happen, cancel any queued flushes - this._debouncedFlush.cancel(); + const eventBuffer = this.eventBuffer; + if (eventBuffer && this.session.segmentId === 0 && !eventBuffer.hasCheckout) { + logInfo('[Replay] Flushing initial segment without checkout.', this._options._experiments.traceInternals); + // TODO FN: Evaluate if we want to stop here, or remove this again? + } // this._flushLock acts as a lock so that future calls to `_flush()` // will be blocked until this promise resolves diff --git a/packages/replay/src/session/fetchSession.ts b/packages/replay/src/session/fetchSession.ts index 2d8f3f98a8aa..43e162b5f3d6 100644 --- a/packages/replay/src/session/fetchSession.ts +++ b/packages/replay/src/session/fetchSession.ts @@ -1,7 +1,7 @@ import { REPLAY_SESSION_KEY, WINDOW } from '../constants'; import type { Session } from '../types'; import { hasSessionStorage } from '../util/hasSessionStorage'; -import { logInfo } from '../util/log'; +import { logInfoNextTick } from '../util/log'; import { makeSession } from './Session'; /** @@ -22,7 +22,7 @@ export function fetchSession(traceInternals?: boolean): Session | null { const sessionObj = JSON.parse(sessionStringFromStorage) as Session; - logInfo('[Replay] Loading existing session', traceInternals); + logInfoNextTick('[Replay] Loading existing session', traceInternals); return makeSession(sessionObj); } catch { diff --git a/packages/replay/src/session/getSession.ts b/packages/replay/src/session/getSession.ts index b26921e58938..da3184f05296 100644 --- a/packages/replay/src/session/getSession.ts +++ b/packages/replay/src/session/getSession.ts @@ -1,6 +1,6 @@ import type { Session, SessionOptions, Timeouts } from '../types'; import { isSessionExpired } from '../util/isSessionExpired'; -import { logInfo } from '../util/log'; +import { logInfoNextTick } from '../util/log'; import { createSession } from './createSession'; import { fetchSession } from './fetchSession'; import { makeSession } from './Session'; @@ -44,10 +44,10 @@ export function getSession({ // and when this session is expired, it will not be renewed until user // reloads. const discardedSession = makeSession({ sampled: false }); - logInfo('[Replay] Session should not be refreshed', traceInternals); + logInfoNextTick('[Replay] Session should not be refreshed', traceInternals); return { type: 'new', session: discardedSession }; } else { - logInfo('[Replay] Session has expired', traceInternals); + logInfoNextTick('[Replay] Session has expired', traceInternals); } // Otherwise continue to create a new session } @@ -57,7 +57,7 @@ export function getSession({ sessionSampleRate, allowBuffering, }); - logInfo('[Replay] Created new session', traceInternals); + logInfoNextTick('[Replay] Created new session', traceInternals); return { type: 'new', session: newSession }; } diff --git a/packages/replay/src/types/replay.ts b/packages/replay/src/types/replay.ts index 46f1e8f4ef93..1fbf44aa1b95 100644 --- a/packages/replay/src/types/replay.ts +++ b/packages/replay/src/types/replay.ts @@ -389,6 +389,11 @@ export interface EventBuffer { */ readonly type: EventBufferType; + /** + * If the event buffer contains a checkout event. + */ + hasCheckout: boolean; + /** * Destroy the event buffer. */ diff --git a/packages/replay/src/util/addEvent.ts b/packages/replay/src/util/addEvent.ts index 982ac3b5374d..d1e1d366a9e9 100644 --- a/packages/replay/src/util/addEvent.ts +++ b/packages/replay/src/util/addEvent.ts @@ -40,8 +40,9 @@ export async function addEvent( } try { - if (isCheckout) { + if (isCheckout && replay.recordingMode === 'buffer') { replay.eventBuffer.clear(); + replay.eventBuffer.hasCheckout = true; } const replayOptions = replay.getOptions(); diff --git a/packages/replay/src/util/log.ts b/packages/replay/src/util/log.ts index b5aca4039429..9aa650a81264 100644 --- a/packages/replay/src/util/log.ts +++ b/packages/replay/src/util/log.ts @@ -12,17 +12,41 @@ export function logInfo(message: string, shouldAddBreadcrumb?: boolean): void { logger.info(message); if (shouldAddBreadcrumb) { - const hub = getCurrentHub(); - hub.addBreadcrumb( - { - category: 'console', - data: { - logger: 'replay', - }, - level: 'info', - message, - }, - { level: 'info' }, - ); + addBreadcrumb(message); + } +} + +/** + * Log a message, and add a breadcrumb in the next tick. + * This is necessary when the breadcrumb may be added before the replay is initialized. + */ +export function logInfoNextTick(message: string, shouldAddBreadcrumb?: boolean): void { + if (!__DEBUG_BUILD__) { + return; } + + logger.info(message); + + if (shouldAddBreadcrumb) { + // Wait a tick here to avoid race conditions for some initial logs + // which may be added before replay is initialized + setTimeout(() => { + addBreadcrumb(message); + }, 0); + } +} + +function addBreadcrumb(message: string): void { + const hub = getCurrentHub(); + hub.addBreadcrumb( + { + category: 'console', + data: { + logger: 'replay', + }, + level: 'info', + message, + }, + { level: 'info' }, + ); } diff --git a/packages/replay/test/integration/flush.test.ts b/packages/replay/test/integration/flush.test.ts index a4fbdb464ae4..611a1043df1a 100644 --- a/packages/replay/test/integration/flush.test.ts +++ b/packages/replay/test/integration/flush.test.ts @@ -1,6 +1,6 @@ import * as SentryUtils from '@sentry/utils'; -import { DEFAULT_FLUSH_MIN_DELAY, WINDOW } from '../../src/constants'; +import { DEFAULT_FLUSH_MIN_DELAY, MAX_SESSION_LIFE, WINDOW } from '../../src/constants'; import type { ReplayContainer } from '../../src/replay'; import { clearSession } from '../../src/session/clearSession'; import type { EventBuffer } from '../../src/types'; @@ -286,15 +286,22 @@ describe('Integration | flush', () => { expect(mockFlush).toHaveBeenCalledTimes(20); expect(mockSendReplay).toHaveBeenCalledTimes(1); + + replay.getOptions().minReplayDuration = 0; }); it('does not flush if session is too long', async () => { replay.timeouts.maxSessionLife = 100_000; - jest.setSystemTime(new Date(BASE_TIMESTAMP)); + jest.setSystemTime(BASE_TIMESTAMP); sessionStorage.clear(); clearSession(replay); replay['_loadAndCheckSession'](); + // No-op _loadAndCheckSession to avoid us resetting the session for this test + const _tmp = replay['_loadAndCheckSession']; + replay['_loadAndCheckSession'] = () => { + return true; + }; await advanceTimers(120_000); @@ -308,7 +315,71 @@ describe('Integration | flush', () => { mockRecord._emitter(TEST_EVENT); await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); + expect(mockFlush).toHaveBeenCalledTimes(1); expect(mockSendReplay).toHaveBeenCalledTimes(0); + + replay.timeouts.maxSessionLife = MAX_SESSION_LIFE; + replay['_loadAndCheckSession'] = _tmp; + }); + + it('logs warning if flushing initial segment without checkout', async () => { + replay.getOptions()._experiments.traceInternals = true; + + sessionStorage.clear(); + clearSession(replay); + replay['_loadAndCheckSession'](); + await new Promise(process.nextTick); + jest.setSystemTime(BASE_TIMESTAMP); + + // Clear the event buffer to simulate no checkout happened + replay.eventBuffer!.clear(); + + // click happens first + domHandler({ + name: 'click', + }); + + // no checkout! + await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); + + expect(mockFlush).toHaveBeenCalledTimes(1); + expect(mockSendReplay).toHaveBeenCalledTimes(1); + + const replayData = mockSendReplay.mock.calls[0][0]; + + expect(JSON.parse(replayData.recordingData)).toEqual([ + { + type: 5, + timestamp: BASE_TIMESTAMP, + data: { + tag: 'breadcrumb', + payload: { + timestamp: BASE_TIMESTAMP / 1000, + type: 'default', + category: 'ui.click', + message: '', + data: {}, + }, + }, + }, + { + type: 5, + timestamp: BASE_TIMESTAMP + DEFAULT_FLUSH_MIN_DELAY, + data: { + tag: 'breadcrumb', + payload: { + timestamp: (BASE_TIMESTAMP + DEFAULT_FLUSH_MIN_DELAY) / 1000, + type: 'default', + category: 'console', + data: { logger: 'replay' }, + level: 'info', + message: '[Replay] Flushing initial segment without checkout.', + }, + }, + }, + ]); + + replay.getOptions()._experiments.traceInternals = false; }); }); diff --git a/packages/tracing-internal/src/node/integrations/prisma.ts b/packages/tracing-internal/src/node/integrations/prisma.ts index fad8672c89a2..c87562dca98b 100644 --- a/packages/tracing-internal/src/node/integrations/prisma.ts +++ b/packages/tracing-internal/src/node/integrations/prisma.ts @@ -84,9 +84,7 @@ export class Prisma implements Integration { }); } else { __DEBUG_BUILD__ && - logger.warn( - `Unsupported Prisma client provided to PrismaIntegration. Provided client: ${JSON.stringify(options.client)}`, - ); + logger.warn('Unsupported Prisma client provided to PrismaIntegration. Provided client:', options.client); } } diff --git a/packages/utils/src/path.ts b/packages/utils/src/path.ts index 16534a7ef358..7a65aa57b7c8 100644 --- a/packages/utils/src/path.ts +++ b/packages/utils/src/path.ts @@ -51,10 +51,13 @@ function normalizeArray(parts: string[], allowAboveRoot?: boolean): string[] { // Split a filename into [root, dir, basename, ext], unix version // 'root' is just a slash, or nothing. -const splitPathRe = /^(\/?|)([\s\S]*?)((?:\.{1,2}|[^/]+?|)(\.[^./]*|))(?:[/]*)$/; +const splitPathRe = /^(\S+:\\|\/?)([\s\S]*?)((?:\.{1,2}|[^/\\]+?|)(\.[^./\\]*|))(?:[/\\]*)$/; /** JSDoc */ function splitPath(filename: string): string[] { - const parts = splitPathRe.exec(filename); + // Truncate files names greater than 1024 characters to avoid regex dos + // https://github.com/getsentry/sentry-javascript/pull/8737#discussion_r1285719172 + const truncated = filename.length > 1024 ? `${filename.slice(-1024)}` : filename; + const parts = splitPathRe.exec(truncated); return parts ? parts.slice(1) : []; } diff --git a/packages/utils/test/path.test.ts b/packages/utils/test/path.test.ts new file mode 100644 index 000000000000..3ba750830f1b --- /dev/null +++ b/packages/utils/test/path.test.ts @@ -0,0 +1,33 @@ +import { basename, dirname } from '../src/path'; + +describe('path', () => { + describe('basename', () => { + test('unix', () => { + expect(basename('/foo/bar/baz/asdf/quux.html')).toEqual('quux.html'); + expect(basename('foo/bar/baz/asdf/quux.html')).toEqual('quux.html'); + expect(basename('../baz/asdf/quux.html')).toEqual('quux.html'); + expect(basename('quux.html')).toEqual('quux.html'); + }); + test('windows', () => { + expect(basename('c:\\foo\\bar\\baz\\asdf\\quux.html')).toEqual('quux.html'); + expect(basename('\\foo\\bar\\baz\\asdf\\quux.html')).toEqual('quux.html'); + expect(basename('..\\bar\\baz\\asdf\\quux.html')).toEqual('quux.html'); + expect(basename('quux.html')).toEqual('quux.html'); + }); + }); + + describe('dirname', () => { + test('unix', () => { + expect(dirname('/foo/bar/baz/asdf/quux.html')).toEqual('/foo/bar/baz/asdf'); + expect(dirname('foo/bar/baz/asdf/quux.html')).toEqual('foo/bar/baz/asdf'); + expect(dirname('../baz/asdf/quux.html')).toEqual('../baz/asdf'); + expect(dirname('/quux.html')).toEqual('/'); + }); + test('windows', () => { + expect(dirname('C:\\foo\\bar\\baz\\asdf\\quux.html')).toEqual('C:\\foo\\bar\\baz\\asdf'); + expect(dirname('\\foo\\bar\\baz\\asdf\\quux.html')).toEqual('\\foo\\bar\\baz\\asdf'); + expect(dirname('..\\bar\\baz\\asdf\\quux.html')).toEqual('..\\bar\\baz\\asdf'); + expect(dirname('quux.html')).toEqual('.'); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index b8336f9c8063..adbbc6797a04 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2304,18 +2304,18 @@ silent-error "^1.1.1" util.promisify "^1.0.0" -"@ember/test-helpers@~2.8.1": - version "2.8.1" - resolved "https://registry.yarnpkg.com/@ember/test-helpers/-/test-helpers-2.8.1.tgz#20f2e30d48172c2ff713e1db7fbec5352f918d4e" - integrity sha512-jbsYwWyAdhL/pdPu7Gb3SG1gvIXY70FWMtC/Us0Kmvk82Y+5YUQ1SOC0io75qmOGYQmH7eQrd/bquEVd+4XtdQ== +"@ember/test-helpers@2.9.4": + version "2.9.4" + resolved "https://registry.yarnpkg.com/@ember/test-helpers/-/test-helpers-2.9.4.tgz#985022e9ba05cfc918bcf08b77cbb355f85b723e" + integrity sha512-z+Qs1NYWyIVDmrY6WdmOS5mdG1lJ5CFfzh6dRhLfs9lq45deDaDrVNcaCYhnNeJZTvUBK2XR2SvPcZm0RloXdA== dependencies: "@ember/test-waiters" "^3.0.0" - "@embroider/macros" "^1.6.0" - "@embroider/util" "^1.6.0" + "@embroider/macros" "^1.10.0" + "@embroider/util" "^1.9.0" broccoli-debug "^0.6.5" broccoli-funnel "^3.0.8" - ember-cli-babel "^7.26.6" - ember-cli-htmlbars "^5.7.1" + ember-cli-babel "^7.26.11" + ember-cli-htmlbars "^6.1.1" ember-destroyable-polyfill "^2.0.3" "@ember/test-waiters@^3.0.0": @@ -2328,7 +2328,7 @@ ember-cli-version-checker "^5.1.2" semver "^7.3.5" -"@embroider/macros@^1.0.0", "@embroider/macros@^1.6.0", "@embroider/macros@^1.9.0": +"@embroider/macros@^1.0.0", "@embroider/macros@^1.9.0": version "1.9.0" resolved "https://registry.yarnpkg.com/@embroider/macros/-/macros-1.9.0.tgz#0df2a56fdd93f11fddea450b6ca83cc2119b5008" integrity sha512-12ElrRT+mX3aSixGHjHnfsnyoH1hw5nM+P+Ax0ITZdp6TaAvWZ8dENnVHltdnv4ssHiX0EsVEXmqbIIdMN4nLA== @@ -2342,6 +2342,20 @@ resolve "^1.20.0" semver "^7.3.2" +"@embroider/macros@^1.10.0", "@embroider/macros@^1.11.0": + version "1.13.1" + resolved "https://registry.yarnpkg.com/@embroider/macros/-/macros-1.13.1.tgz#aee17e5af0e0086bd36873bdb4e49ea346bab3fa" + integrity sha512-4htraP/rNIht8uCxXoc59Bw2EsBFfc4YUQD9XSpzJ4xUr1V0GQf9wL/noeSuYSxIhwRfZOErnJhsdyf1hH+I/A== + dependencies: + "@embroider/shared-internals" "2.4.0" + assert-never "^1.2.1" + babel-import-util "^2.0.0" + ember-cli-babel "^7.26.6" + find-up "^5.0.0" + lodash "^4.17.21" + resolve "^1.20.0" + semver "^7.3.2" + "@embroider/shared-internals@1.8.3": version "1.8.3" resolved "https://registry.yarnpkg.com/@embroider/shared-internals/-/shared-internals-1.8.3.tgz#52d868dc80016e9fe983552c0e516f437bf9b9f9" @@ -2356,6 +2370,21 @@ semver "^7.3.5" typescript-memoize "^1.0.1" +"@embroider/shared-internals@2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@embroider/shared-internals/-/shared-internals-2.4.0.tgz#0e9fdb0b2df9bad45fab8c54cbb70d8a2cbf01fc" + integrity sha512-pFE05ebenWMC9XAPRjadYCXXb6VmqjkhYN5uqkhPo+VUmMHnx7sZYYxqGjxfVuhC/ghS/BNlOffOCXDOoE7k7g== + dependencies: + babel-import-util "^2.0.0" + debug "^4.3.2" + ember-rfc176-data "^0.3.17" + fs-extra "^9.1.0" + js-string-escape "^1.0.1" + lodash "^4.17.21" + resolve-package-path "^4.0.1" + semver "^7.3.5" + typescript-memoize "^1.0.1" + "@embroider/shared-internals@^2.0.0": version "2.3.0" resolved "https://registry.yarnpkg.com/@embroider/shared-internals/-/shared-internals-2.3.0.tgz#97215f6263a4013fbdb3d1e4890cc069f2d9df12" @@ -2379,14 +2408,14 @@ lodash "^4.17.21" resolve "^1.20.0" -"@embroider/util@^1.6.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@embroider/util/-/util-1.9.0.tgz#331c46bdf106c44cb1dd6baaa9030d322c13cfca" - integrity sha512-9I63iJK6N01OHJafmS/BX0msUkTlmhFMIEmDl/SRNACVi0nS6QfNyTgTTeji1P/DALf6eobg/9t/N4VhS9G9QA== +"@embroider/util@^1.9.0": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@embroider/util/-/util-1.11.1.tgz#622390932542e6b7f8d5d28e956891306e664eb3" + integrity sha512-IqzlEQahM2cfLvo4PULA2WyvROqr9jRmeSv0GGZzpitWCh6l4FDwweOLSArdlKSXdQxHkKhwBMCi//7DhKjRlg== dependencies: - "@embroider/macros" "^1.9.0" + "@embroider/macros" "^1.11.0" broccoli-funnel "^3.0.5" - ember-cli-babel "^7.23.1" + ember-cli-babel "^7.26.11" "@esbuild/android-arm64@0.16.17": version "0.16.17" @@ -2498,13 +2527,18 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.16.17.tgz#c5a1a4bfe1b57f0c3e61b29883525c6da3e5c091" integrity sha512-y+EHuSchhL7FjHgvQL/0fnnFmO4T1bhvWANX6gcnqTjtnKWbTvUMCpGnv2+t+31d7RzyEAYAd4u2fnIhHL6N/Q== -"@eslint-community/eslint-utils@^4.2.0": +"@eslint-community/eslint-utils@^4.1.2", "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== dependencies: eslint-visitor-keys "^3.3.0" +"@eslint-community/regexpp@^4.5.0": + version "4.6.2" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.6.2.tgz#1816b5f6948029c5eaacb0703b850ee0cb37d8f8" + integrity sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw== + "@eslint/eslintrc@^0.4.3": version "0.4.3" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz#9e42981ef035beb3dd49add17acb96e8ff6f394c" @@ -2562,6 +2596,13 @@ dependencies: "@glimmer/env" "^0.1.7" +"@glimmer/global-context@0.84.3": + version "0.84.3" + resolved "https://registry.yarnpkg.com/@glimmer/global-context/-/global-context-0.84.3.tgz#f8bf2cda9562716f2ddf3f96837e7559600635c4" + integrity sha512-8Oy9Wg5IZxMEeAnVmzD2NkObf89BeHoFSzJgJROE/deutd3rxg83mvlOez4zBBGYwnTb+VGU2LYRpet92egJjA== + dependencies: + "@glimmer/env" "^0.1.7" + "@glimmer/interfaces@0.83.1": version "0.83.1" resolved "https://registry.yarnpkg.com/@glimmer/interfaces/-/interfaces-0.83.1.tgz#fb16f5f683ddc55f130887b6141f58c0751350fe" @@ -2587,6 +2628,17 @@ "@glimmer/util" "0.83.1" "@glimmer/validator" "0.83.1" +"@glimmer/reference@^0.84.3": + version "0.84.3" + resolved "https://registry.yarnpkg.com/@glimmer/reference/-/reference-0.84.3.tgz#6420ad9c102633ac83939fd1b2457269d21fb632" + integrity sha512-lV+p/aWPVC8vUjmlvYVU7WQJsLh319SdXuAWoX/SE3pq340BJlAJiEcAc6q52y9JNhT57gMwtjMX96W5Xcx/qw== + dependencies: + "@glimmer/env" "^0.1.7" + "@glimmer/global-context" "0.84.3" + "@glimmer/interfaces" "0.84.3" + "@glimmer/util" "0.84.3" + "@glimmer/validator" "0.84.3" + "@glimmer/syntax@^0.83.1": version "0.83.1" resolved "https://registry.yarnpkg.com/@glimmer/syntax/-/syntax-0.83.1.tgz#7e18dd445871c157ba0281f12a4fbf316fa49b41" @@ -2597,7 +2649,7 @@ "@handlebars/parser" "~2.0.0" simple-html-tokenizer "^0.5.11" -"@glimmer/syntax@^0.84.3": +"@glimmer/syntax@^0.84.2", "@glimmer/syntax@^0.84.3": version "0.84.3" resolved "https://registry.yarnpkg.com/@glimmer/syntax/-/syntax-0.84.3.tgz#4045a1708cef7fd810cff42fe6deeba40c7286d0" integrity sha512-ioVbTic6ZisLxqTgRBL2PCjYZTFIwobifCustrozRU2xGDiYvVIL0vt25h2c1ioDsX59UgVlDkIK4YTAQQSd2A== @@ -2646,6 +2698,14 @@ "@glimmer/env" "^0.1.7" "@glimmer/global-context" "0.83.1" +"@glimmer/validator@0.84.3", "@glimmer/validator@^0.84.3": + version "0.84.3" + resolved "https://registry.yarnpkg.com/@glimmer/validator/-/validator-0.84.3.tgz#cd83b7f9ab78953f23cc11a32d83d7f729c54df2" + integrity sha512-RTBV4TokUB0vI31UC7ikpV7lOYpWUlyqaKV//pRC4pexYMlmqnVhkFrdiimB/R1XyNdUOQUmnIAcdic39NkbhQ== + dependencies: + "@glimmer/env" "^0.1.7" + "@glimmer/global-context" "0.84.3" + "@glimmer/validator@^0.44.0": version "0.44.0" resolved "https://registry.yarnpkg.com/@glimmer/validator/-/validator-0.44.0.tgz#03d127097dc9cb23052cdb7fcae59d0a9dca53e1" @@ -4833,6 +4893,14 @@ "@types/ember-test-helpers" "*" "@types/qunit" "*" +"@types/ember-resolver@5.0.13": + version "5.0.13" + resolved "https://registry.yarnpkg.com/@types/ember-resolver/-/ember-resolver-5.0.13.tgz#db66678076ca625ed80b629c09619ae85c1c1f7a" + integrity sha512-pO964cAPhAaFJoS28M8+b5MzAhQ/tVuNM4GDUIAexheQat36axG2WTG8LQ5ea07MSFPesrRFk2T3z88pfvdYKA== + dependencies: + "@types/ember__object" "*" + "@types/ember__owner" "*" + "@types/ember-test-helpers@*": version "1.0.9" resolved "https://registry.yarnpkg.com/@types/ember-test-helpers/-/ember-test-helpers-1.0.9.tgz#4279c5f3b390f25fbfb3f9f210785d36a336b8a7" @@ -4938,6 +5006,11 @@ "@types/ember__object" "*" "@types/rsvp" "*" +"@types/ember__owner@*": + version "4.0.4" + resolved "https://registry.yarnpkg.com/@types/ember__owner/-/ember__owner-4.0.4.tgz#f118ef4cdcca62c39426aa8032280d45b912148a" + integrity sha512-FD0XuAlIfeVEwpKcAeGczQxa6D0huKxvPHuPE+FIm+zWZmqnI6yhxDhZgeGjnhmCCLAHRp8+1HRoKOFwnmaW3Q== + "@types/ember__polyfills@*": version "3.12.1" resolved "https://registry.yarnpkg.com/@types/ember__polyfills/-/ember__polyfills-3.12.1.tgz#aed838e35a3e8670d247333d4c7ea2c2f7b3c43e" @@ -4980,16 +5053,6 @@ resolved "https://registry.yarnpkg.com/@types/ember__template/-/ember__template-3.16.1.tgz#30d7f50a49b190934db0f5a56dd76ad86c21efc6" integrity sha512-APQINizzizl2LHWGMFBCanRjKZQsdzqn7b+us17zbNhnx/R0IZAJq901x/i7eozCRwxsDKmGzNABSCIu6uc1Tg== -"@types/ember__test-helpers@~1.7.0": - version "1.7.5" - resolved "https://registry.yarnpkg.com/@types/ember__test-helpers/-/ember__test-helpers-1.7.5.tgz#6a9f5e517869fa396dc037f729f1b56a00bf5d92" - integrity sha512-Hs3/9DTwJp8QPr2Jt8ZOuoxogSFH4Agi++mszutzd8GLk9Skeo1nN5IJY5FjaS8j6eDAYaPv71sRvuRa+CNA7A== - dependencies: - "@types/ember" "*" - "@types/ember__application" "*" - "@types/ember__error" "*" - "@types/htmlbars-inline-precompile" "*" - "@types/ember__test@*": version "3.16.1" resolved "https://registry.yarnpkg.com/@types/ember__test/-/ember__test-3.16.1.tgz#8407e42b9835a13ef0c6ef7a7ce3aa3d7ebcb7ed" @@ -7495,7 +7558,7 @@ babel-plugin-filter-imports@^4.0.0: "@babel/types" "^7.7.2" lodash "^4.17.15" -babel-plugin-htmlbars-inline-precompile@^5.0.0, babel-plugin-htmlbars-inline-precompile@^5.2.1, babel-plugin-htmlbars-inline-precompile@^5.3.0: +babel-plugin-htmlbars-inline-precompile@^5.2.1, babel-plugin-htmlbars-inline-precompile@^5.3.0: version "5.3.1" resolved "https://registry.yarnpkg.com/babel-plugin-htmlbars-inline-precompile/-/babel-plugin-htmlbars-inline-precompile-5.3.1.tgz#5ba272e2e4b6221522401f5f1d98a73b1de38787" integrity sha512-QWjjFgSKtSRIcsBhJmEwS2laIdrA6na8HAlc/pEAhjHgQsah/gMiBFRZvbQTy//hWxR4BMwV7/Mya7q5H8uHeA== @@ -8965,7 +9028,7 @@ builtins@^1.0.3: resolved "https://registry.yarnpkg.com/builtins/-/builtins-1.0.3.tgz#cb94faeb61c8696451db36534e1422f94f0aee88" integrity sha1-y5T662HIaWRR2zZTThQi+U8K7og= -builtins@^5.0.0: +builtins@^5.0.0, builtins@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/builtins/-/builtins-5.0.1.tgz#87f6db9ab0458be728564fa81d876d8d74552fa9" integrity sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ== @@ -11708,7 +11771,7 @@ ember-cli-babel-plugin-helpers@^1.0.0, ember-cli-babel-plugin-helpers@^1.1.1: resolved "https://registry.yarnpkg.com/ember-cli-babel-plugin-helpers/-/ember-cli-babel-plugin-helpers-1.1.1.tgz#5016b80cdef37036c4282eef2d863e1d73576879" integrity sha512-sKvOiPNHr5F/60NLd7SFzMpYPte/nnGkq/tMIfXejfKHIhaiIkYFqX8Z9UFTKWLLn+V7NOaby6niNPZUdvKCRw== -ember-cli-babel@^7.0.0, ember-cli-babel@^7.13.0, ember-cli-babel@^7.13.2, ember-cli-babel@^7.22.1, ember-cli-babel@^7.23.0, ember-cli-babel@^7.23.1, ember-cli-babel@^7.26.11, ember-cli-babel@^7.26.4, ember-cli-babel@^7.26.6, ember-cli-babel@^7.7.3: +ember-cli-babel@^7.0.0, ember-cli-babel@^7.13.0, ember-cli-babel@^7.13.2, ember-cli-babel@^7.22.1, ember-cli-babel@^7.23.0, ember-cli-babel@^7.26.11, ember-cli-babel@^7.26.4, ember-cli-babel@^7.26.6, ember-cli-babel@^7.7.3: version "7.26.11" resolved "https://registry.yarnpkg.com/ember-cli-babel/-/ember-cli-babel-7.26.11.tgz#50da0fe4dcd99aada499843940fec75076249a9f" integrity sha512-JJYeYjiz/JTn34q7F5DSOjkkZqy8qwFOOxXfE6pe9yEJqWGu4qErKxlz8I22JoVEQ/aBUO+OcKTpmctvykM9YA== @@ -11760,28 +11823,6 @@ ember-cli-get-component-path-option@^1.0.0: resolved "https://registry.yarnpkg.com/ember-cli-get-component-path-option/-/ember-cli-get-component-path-option-1.0.0.tgz#0d7b595559e2f9050abed804f1d8eff1b08bc771" integrity sha1-DXtZVVni+QUKvtgE8djv8bCLx3E= -ember-cli-htmlbars@^5.7.1: - version "5.7.2" - resolved "https://registry.yarnpkg.com/ember-cli-htmlbars/-/ember-cli-htmlbars-5.7.2.tgz#e0cd2fb3c20d85fe4c3e228e6f0590ee1c645ba8" - integrity sha512-Uj6R+3TtBV5RZoJY14oZn/sNPnc+UgmC8nb5rI4P3fR/gYoyTFIZSXiIM7zl++IpMoIrocxOrgt+mhonKphgGg== - dependencies: - "@ember/edition-utils" "^1.2.0" - babel-plugin-htmlbars-inline-precompile "^5.0.0" - broccoli-debug "^0.6.5" - broccoli-persistent-filter "^3.1.2" - broccoli-plugin "^4.0.3" - common-tags "^1.8.0" - ember-cli-babel-plugin-helpers "^1.1.1" - ember-cli-version-checker "^5.1.2" - fs-tree-diff "^2.0.1" - hash-for-dep "^1.5.1" - heimdalljs-logger "^0.1.10" - json-stable-stringify "^1.0.1" - semver "^7.3.4" - silent-error "^1.1.1" - strip-bom "^4.0.0" - walk-sync "^2.2.0" - ember-cli-htmlbars@^6.0.1, ember-cli-htmlbars@^6.1.1: version "6.1.1" resolved "https://registry.yarnpkg.com/ember-cli-htmlbars/-/ember-cli-htmlbars-6.1.1.tgz#f5b588572a5d18ad087560122b8dabc90145173d" @@ -12254,6 +12295,21 @@ ember-template-imports@^3.1.1: string.prototype.matchall "^4.0.6" validate-peer-dependencies "^1.1.0" +ember-template-imports@^3.4.2: + version "3.4.2" + resolved "https://registry.yarnpkg.com/ember-template-imports/-/ember-template-imports-3.4.2.tgz#6cf7de7d4b8348a0fddf3aaec4947aa1211289e6" + integrity sha512-OS8TUVG2kQYYwP3netunLVfeijPoOKIs1SvPQRTNOQX4Pu8xGGBEZmrv0U1YTnQn12Eg+p6w/0UdGbUnITjyzw== + dependencies: + babel-import-util "^0.2.0" + broccoli-stew "^3.0.0" + ember-cli-babel-plugin-helpers "^1.1.1" + ember-cli-version-checker "^5.1.2" + line-column "^1.0.2" + magic-string "^0.25.7" + parse-static-imports "^1.1.0" + string.prototype.matchall "^4.0.6" + validate-peer-dependencies "^1.1.0" + ember-template-lint@~4.16.1: version "4.16.1" resolved "https://registry.yarnpkg.com/ember-template-lint/-/ember-template-lint-4.16.1.tgz#16d59916b1b1f2c5f0fd4bc86a1c4d91b3ae2c24" @@ -12294,6 +12350,23 @@ ember-template-recast@^6.1.3: tmp "^0.2.1" workerpool "^6.1.5" +ember-template-recast@^6.1.4: + version "6.1.4" + resolved "https://registry.yarnpkg.com/ember-template-recast/-/ember-template-recast-6.1.4.tgz#e964c184adfd876878009f8aa0b84c95633fce20" + integrity sha512-fCh+rOK6z+/tsdkTbOE+e7f84P6ObnIRQrCCrnu21E4X05hPeradikIkRMhJdxn4NWrxitfZskQDd37TR/lsNQ== + dependencies: + "@glimmer/reference" "^0.84.3" + "@glimmer/syntax" "^0.84.3" + "@glimmer/validator" "^0.84.3" + async-promise-queue "^1.0.5" + colors "^1.4.0" + commander "^8.3.0" + globby "^11.0.3" + ora "^5.4.0" + slash "^3.0.0" + tmp "^0.2.1" + workerpool "^6.4.0" + ember-test-selectors@~6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/ember-test-selectors/-/ember-test-selectors-6.0.0.tgz#ba9bb19550d9dec6e4037d86d2b13c2cfd329341" @@ -12857,27 +12930,32 @@ eslint-plugin-deprecation@^1.1.0: tslib "^1.10.0" tsutils "^3.0.0" -eslint-plugin-ember@~11.1.0: - version "11.1.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-ember/-/eslint-plugin-ember-11.1.0.tgz#98349676f2b5e317cdd9207ce9f65036e3ec7c9a" - integrity sha512-g1pDwgw2sUTJDfbFVoI5u6fbhs2v0jrTiq5cChQ0DqzTqZchlPtCj7ySSFrqfcSp8MLOuX2bx8lOH9uKeb5N1w== +eslint-plugin-ember@11.9.0: + version "11.9.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-ember/-/eslint-plugin-ember-11.9.0.tgz#b02f8d7c2d78ff2b8f13d1eaff55d13edfa8cfee" + integrity sha512-kpsvbdQOFw9ikzwmhxR8mmsAXtwwj+DPMGn1NOHAHJOnhTkyioKEiNO6tQK/b33VtQy1VpJP6zFu+Bt6m/cYxA== dependencies: "@ember-data/rfc395-data" "^0.0.4" + "@glimmer/syntax" "^0.84.2" css-tree "^2.0.4" ember-rfc176-data "^0.3.15" + ember-template-imports "^3.4.2" + ember-template-recast "^6.1.4" eslint-utils "^3.0.0" estraverse "^5.2.0" + lodash.camelcase "^4.1.1" lodash.kebabcase "^4.1.1" + magic-string "^0.30.0" requireindex "^1.2.0" snake-case "^3.0.3" -eslint-plugin-es@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz#75a7cdfdccddc0589934aeeb384175f221c57893" - integrity sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ== +eslint-plugin-es-x@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-es-x/-/eslint-plugin-es-x-7.1.0.tgz#f0d5421e658cca95c1cfb2355831851bdc83322d" + integrity sha512-AhiaF31syh4CCQ+C5ccJA0VG6+kJK8+5mXKKE7Qs1xcPRg02CDPOj3mWlQxuWS/AYtg7kxrDNgW9YW3vc0Q+Mw== dependencies: - eslint-utils "^2.0.0" - regexpp "^3.0.0" + "@eslint-community/eslint-utils" "^4.1.2" + "@eslint-community/regexpp" "^4.5.0" eslint-plugin-import@^2.22.0: version "2.22.1" @@ -12918,22 +12996,24 @@ eslint-plugin-jsdoc@^30.0.3: semver "^7.3.4" spdx-expression-parse "^3.0.1" -eslint-plugin-node@~11.1.0: - version "11.1.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz#c95544416ee4ada26740a30474eefc5402dc671d" - integrity sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g== +eslint-plugin-n@16.0.1: + version "16.0.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-n/-/eslint-plugin-n-16.0.1.tgz#baa62bb3af52940a53ba15386348ad9b0b425ada" + integrity sha512-CDmHegJN0OF3L5cz5tATH84RPQm9kG+Yx39wIqIwPR2C0uhBGMWfbbOtetR83PQjjidA5aXMu+LEFw1jaSwvTA== dependencies: - eslint-plugin-es "^3.0.0" - eslint-utils "^2.0.0" - ignore "^5.1.1" - minimatch "^3.0.4" - resolve "^1.10.1" - semver "^6.1.0" + "@eslint-community/eslint-utils" "^4.4.0" + builtins "^5.0.1" + eslint-plugin-es-x "^7.1.0" + ignore "^5.2.4" + is-core-module "^2.12.1" + minimatch "^3.1.2" + resolve "^1.22.2" + semver "^7.5.3" -eslint-plugin-qunit@~7.3.1: - version "7.3.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-qunit/-/eslint-plugin-qunit-7.3.1.tgz#cb7c0012199a8db8ff43916d07b0361e999a53b1" - integrity sha512-L1yutkLqCgr70ZmMAbBKPvUOUwhKryZ0RaJKOzw72Bmn8no3JNBL9hhbX2aTvfZqYM/wLXIT0nICZiGrV4xVJw== +eslint-plugin-qunit@8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-qunit/-/eslint-plugin-qunit-8.0.0.tgz#92df9b8cc144a67edaf961e9c4db75d98065ce85" + integrity sha512-ly2x/pmJPcS0ztGAPap6qLC13GjOFwhBbvun0K1dAjaxaC6KB3TYjeBo+5pGvXqL3WdicmYxEKhTGwmhvoxMBQ== dependencies: eslint-utils "^3.0.0" requireindex "^1.2.0" @@ -15625,6 +15705,11 @@ ignore@^5.0.4, ignore@^5.1.1, ignore@^5.2.0: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.1.tgz#c2b1f76cb999ede1502f3a226a9310fdfe88d46c" integrity sha512-d2qQLzTJ9WxQftPAuEQpSPmKqzxePjzVbpAVv62AQ64NTL+wR4JkrVqR/LqFsFEUsHDAiId52mJteHDFuDkElA== +ignore@^5.2.4: + version "5.2.4" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" + integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== + image-size@~0.5.0: version "0.5.5" resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c" @@ -16067,6 +16152,13 @@ is-color-stop@^1.0.0: rgb-regex "^1.0.1" rgba-regex "^1.0.0" +is-core-module@^2.11.0, is-core-module@^2.12.1, is-core-module@^2.5.0: + version "2.12.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.12.1.tgz#0c0b6885b6f80011c71541ce15c8d66cf5a4f9fd" + integrity sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg== + dependencies: + has "^1.0.3" + is-core-module@^2.2.0, is-core-module@^2.8.1, is-core-module@^2.9.0: version "2.11.0" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.11.0.tgz#ad4cb3e3863e814523c96f3f58d26cc570ff0144" @@ -16074,13 +16166,6 @@ is-core-module@^2.2.0, is-core-module@^2.8.1, is-core-module@^2.9.0: dependencies: has "^1.0.3" -is-core-module@^2.5.0: - version "2.12.1" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.12.1.tgz#0c0b6885b6f80011c71541ce15c8d66cf5a4f9fd" - integrity sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg== - dependencies: - has "^1.0.3" - is-data-descriptor@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" @@ -18218,7 +18303,7 @@ lodash.assignin@^4.1.0: resolved "https://registry.yarnpkg.com/lodash.assignin/-/lodash.assignin-4.2.0.tgz#ba8df5fb841eb0a3e8044232b0e263a8dc6a28a2" integrity sha1-uo31+4QesKPoBEIysOJjqNxqKKI= -lodash.camelcase@^4.3.0: +lodash.camelcase@^4.1.1, lodash.camelcase@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY= @@ -23749,7 +23834,7 @@ regexp.prototype.flags@^1.2.0, regexp.prototype.flags@^1.4.3: define-properties "^1.1.3" functions-have-names "^1.2.2" -regexpp@^3.0.0, regexpp@^3.1.0, regexpp@^3.2.0: +regexpp@^3.1.0, regexpp@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== @@ -24137,7 +24222,7 @@ resolve@1.20.0: is-core-module "^2.2.0" path-parse "^1.0.6" -resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.10.1, resolve@^1.11.1, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.14.2, resolve@^1.17.0, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.22.0, resolve@^1.22.1, resolve@^1.3.2, resolve@^1.4.0, resolve@^1.5.0, resolve@^1.8.1: +resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.11.1, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.14.2, resolve@^1.17.0, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.22.0, resolve@^1.22.1, resolve@^1.3.2, resolve@^1.4.0, resolve@^1.5.0, resolve@^1.8.1: version "1.22.1" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== @@ -24146,6 +24231,15 @@ resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.10.1, resolve@^1.11. path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" +resolve@^1.22.2: + version "1.22.2" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.2.tgz#0ed0943d4e301867955766c9f3e1ae6d01c6845f" + integrity sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g== + dependencies: + is-core-module "^2.11.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + resolve@^2.0.0-next.3: version "2.0.0-next.3" resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.3.tgz#d41016293d4a8586a39ca5d9b5f15cbea1f55e46" @@ -24718,14 +24812,14 @@ semver@7.5.3: dependencies: lru-cache "^6.0.0" -semver@7.x, semver@^7.0.0, semver@^7.1.1, semver@^7.1.3, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.1: +semver@7.x, semver@^7.0.0, semver@^7.1.1, semver@^7.1.3, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.1, semver@^7.5.3: version "7.5.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== dependencies: lru-cache "^6.0.0" -semver@^6.0.0, semver@^6.1.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0: +semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0: version "6.3.1" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== @@ -28589,6 +28683,11 @@ workerpool@^6.1.5, workerpool@^6.2.1: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== +workerpool@^6.4.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.4.0.tgz#f8d5cfb45fde32fa3b7af72ad617c3369567a462" + integrity sha512-i3KR1mQMNwY2wx20ozq2EjISGtQWDIfV56We+yGJ5yDs8jTwQiLLaqHlkBHITlCuJnYlVRmXegxFxZg7gqI++A== + "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"