From e41ce248471ca6c7f66be6ec9384950c5ff13e53 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 19 Jun 2023 13:37:43 +0200 Subject: [PATCH 1/4] meta(changelog): Update changelog for 7.56.0 --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ceb218a3a05..da270fb1ccec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 7.56.0 + +- feat(replay): Rework slow click & multi click detection (#8322) +- feat(replay): Stop replay when event buffer exceeds max. size (#8315) +- feat(replay): Consider `window.open` for slow clicks (#8308) +- fix(core): Temporarily store debug IDs in stack frame and only put them into `debug_meta` before sending (#8347) +- fix(remix): Extract deferred responses correctly in root loaders. (#8305) +- fix(vue): Don't call `next` in Vue router 4 instrumentation (#8351) + ## 7.55.2 - fix(replay): Stop exporting `EventType` from `@sentry-internal/rrweb` (#8334) From 527bea7483f9e20b14a6e5187787f911feaeecd5 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Mon, 19 Jun 2023 13:28:05 +0000 Subject: [PATCH 2/4] release: 7.56.0 --- lerna.json | 2 +- packages/angular-ivy/package.json | 8 ++++---- packages/angular/package.json | 8 ++++---- packages/browser-integration-tests/package.json | 2 +- packages/browser/package.json | 14 +++++++------- packages/core/package.json | 6 +++--- packages/core/src/version.ts | 2 +- packages/e2e-tests/package.json | 2 +- packages/ember/package.json | 8 ++++---- packages/eslint-config-sdk/package.json | 6 +++--- packages/eslint-plugin-sdk/package.json | 2 +- packages/gatsby/package.json | 10 +++++----- packages/hub/package.json | 8 ++++---- packages/integration-shims/package.json | 4 ++-- packages/integrations/package.json | 8 ++++---- packages/nextjs/package.json | 14 +++++++------- packages/node-integration-tests/package.json | 2 +- packages/node/package.json | 10 +++++----- packages/opentelemetry-node/package.json | 10 +++++----- packages/overhead-metrics/package.json | 2 +- packages/react/package.json | 8 ++++---- packages/remix/package.json | 12 ++++++------ packages/replay-worker/package.json | 2 +- packages/replay/package.json | 10 +++++----- packages/serverless/package.json | 10 +++++----- packages/svelte/package.json | 8 ++++---- packages/sveltekit/package.json | 16 ++++++++-------- packages/tracing-internal/package.json | 8 ++++---- packages/tracing/package.json | 14 +++++++------- packages/types/package.json | 2 +- packages/typescript/package.json | 2 +- packages/utils/package.json | 4 ++-- packages/vue/package.json | 10 +++++----- packages/wasm/package.json | 8 ++++---- 34 files changed, 121 insertions(+), 121 deletions(-) diff --git a/lerna.json b/lerna.json index 3628d7861208..5485606bb926 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "7.55.2", + "version": "7.56.0", "npmClient": "yarn", "useWorkspaces": true } diff --git a/packages/angular-ivy/package.json b/packages/angular-ivy/package.json index 75d9eb5e4626..b18a7e014fe3 100644 --- a/packages/angular-ivy/package.json +++ b/packages/angular-ivy/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/angular-ivy", - "version": "7.55.2", + "version": "7.56.0", "description": "Official Sentry SDK for Angular with full Ivy Support", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/angular-ivy", @@ -21,9 +21,9 @@ "rxjs": "^6.5.5 || ^7.x" }, "dependencies": { - "@sentry/browser": "7.55.2", - "@sentry/types": "7.55.2", - "@sentry/utils": "7.55.2", + "@sentry/browser": "7.56.0", + "@sentry/types": "7.56.0", + "@sentry/utils": "7.56.0", "tslib": "^2.3.0" }, "devDependencies": { diff --git a/packages/angular/package.json b/packages/angular/package.json index 773361801b90..39a12c3303a7 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/angular", - "version": "7.55.2", + "version": "7.56.0", "description": "Official Sentry SDK for Angular", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/angular", @@ -21,9 +21,9 @@ "rxjs": "^6.5.5 || ^7.x" }, "dependencies": { - "@sentry/browser": "7.55.2", - "@sentry/types": "7.55.2", - "@sentry/utils": "7.55.2", + "@sentry/browser": "7.56.0", + "@sentry/types": "7.56.0", + "@sentry/utils": "7.56.0", "tslib": "^2.0.0" }, "devDependencies": { diff --git a/packages/browser-integration-tests/package.json b/packages/browser-integration-tests/package.json index 4135f14e0ec8..63e61ebf57b7 100644 --- a/packages/browser-integration-tests/package.json +++ b/packages/browser-integration-tests/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/browser-integration-tests", - "version": "7.55.2", + "version": "7.56.0", "main": "index.js", "license": "MIT", "engines": { diff --git a/packages/browser/package.json b/packages/browser/package.json index 199d52b0f714..d59b14ce1b48 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/browser", - "version": "7.55.2", + "version": "7.56.0", "description": "Official Sentry SDK for browsers", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/browser", @@ -16,15 +16,15 @@ "access": "public" }, "dependencies": { - "@sentry-internal/tracing": "7.55.2", - "@sentry/core": "7.55.2", - "@sentry/replay": "7.55.2", - "@sentry/types": "7.55.2", - "@sentry/utils": "7.55.2", + "@sentry-internal/tracing": "7.56.0", + "@sentry/core": "7.56.0", + "@sentry/replay": "7.56.0", + "@sentry/types": "7.56.0", + "@sentry/utils": "7.56.0", "tslib": "^1.9.3" }, "devDependencies": { - "@sentry-internal/integration-shims": "7.55.2", + "@sentry-internal/integration-shims": "7.56.0", "@types/md5": "2.1.33", "btoa": "^1.2.1", "chai": "^4.1.2", diff --git a/packages/core/package.json b/packages/core/package.json index 8ff612ce2d24..2464552af0f8 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/core", - "version": "7.55.2", + "version": "7.56.0", "description": "Base implementation for all Sentry JavaScript SDKs", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/core", @@ -16,8 +16,8 @@ "access": "public" }, "dependencies": { - "@sentry/types": "7.55.2", - "@sentry/utils": "7.55.2", + "@sentry/types": "7.56.0", + "@sentry/utils": "7.56.0", "tslib": "^1.9.3" }, "scripts": { diff --git a/packages/core/src/version.ts b/packages/core/src/version.ts index caf9d9d03f45..c265c7ab0178 100644 --- a/packages/core/src/version.ts +++ b/packages/core/src/version.ts @@ -1 +1 @@ -export const SDK_VERSION = '7.55.2'; +export const SDK_VERSION = '7.56.0'; diff --git a/packages/e2e-tests/package.json b/packages/e2e-tests/package.json index e4b7b6459502..13560617e1b2 100644 --- a/packages/e2e-tests/package.json +++ b/packages/e2e-tests/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/e2e-tests", - "version": "7.55.2", + "version": "7.56.0", "license": "MIT", "engines": { "node": ">=10" diff --git a/packages/ember/package.json b/packages/ember/package.json index f43eaebd7624..2e184fc17a7e 100644 --- a/packages/ember/package.json +++ b/packages/ember/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/ember", - "version": "7.55.2", + "version": "7.56.0", "description": "Official Sentry SDK for Ember.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/ember", @@ -29,9 +29,9 @@ }, "dependencies": { "@embroider/macros": "^1.9.0", - "@sentry/browser": "7.55.2", - "@sentry/types": "7.55.2", - "@sentry/utils": "7.55.2", + "@sentry/browser": "7.56.0", + "@sentry/types": "7.56.0", + "@sentry/utils": "7.56.0", "ember-auto-import": "^1.12.1 || ^2.4.3", "ember-cli-babel": "^7.26.11", "ember-cli-htmlbars": "^6.1.1", diff --git a/packages/eslint-config-sdk/package.json b/packages/eslint-config-sdk/package.json index dca0fab40c70..bc4fb7a8c778 100644 --- a/packages/eslint-config-sdk/package.json +++ b/packages/eslint-config-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/eslint-config-sdk", - "version": "7.55.2", + "version": "7.56.0", "description": "Official Sentry SDK eslint config", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/eslint-config-sdk", @@ -19,8 +19,8 @@ "access": "public" }, "dependencies": { - "@sentry-internal/eslint-plugin-sdk": "7.55.2", - "@sentry-internal/typescript": "7.55.2", + "@sentry-internal/eslint-plugin-sdk": "7.56.0", + "@sentry-internal/typescript": "7.56.0", "@typescript-eslint/eslint-plugin": "^5.48.0", "@typescript-eslint/parser": "^5.48.0", "eslint-config-prettier": "^6.11.0", diff --git a/packages/eslint-plugin-sdk/package.json b/packages/eslint-plugin-sdk/package.json index 9d51c40cd436..12fa738a0be4 100644 --- a/packages/eslint-plugin-sdk/package.json +++ b/packages/eslint-plugin-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/eslint-plugin-sdk", - "version": "7.55.2", + "version": "7.56.0", "description": "Official Sentry SDK eslint plugin", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/eslint-plugin-sdk", diff --git a/packages/gatsby/package.json b/packages/gatsby/package.json index 0af92c72f520..337af4a11f32 100644 --- a/packages/gatsby/package.json +++ b/packages/gatsby/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/gatsby", - "version": "7.55.2", + "version": "7.56.0", "description": "Official Sentry SDK for Gatsby.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/gatsby", @@ -20,10 +20,10 @@ "access": "public" }, "dependencies": { - "@sentry/core": "7.55.2", - "@sentry/react": "7.55.2", - "@sentry/types": "7.55.2", - "@sentry/utils": "7.55.2", + "@sentry/core": "7.56.0", + "@sentry/react": "7.56.0", + "@sentry/types": "7.56.0", + "@sentry/utils": "7.56.0", "@sentry/webpack-plugin": "1.19.0" }, "peerDependencies": { diff --git a/packages/hub/package.json b/packages/hub/package.json index af288ce8d529..ca34143369db 100644 --- a/packages/hub/package.json +++ b/packages/hub/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/hub", - "version": "7.55.2", + "version": "7.56.0", "description": "Sentry hub which handles global state managment.", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/hub", @@ -16,9 +16,9 @@ "access": "public" }, "dependencies": { - "@sentry/core": "7.55.2", - "@sentry/types": "7.55.2", - "@sentry/utils": "7.55.2", + "@sentry/core": "7.56.0", + "@sentry/types": "7.56.0", + "@sentry/utils": "7.56.0", "tslib": "^1.9.3" }, "scripts": { diff --git a/packages/integration-shims/package.json b/packages/integration-shims/package.json index b1a776879cdb..472198466754 100644 --- a/packages/integration-shims/package.json +++ b/packages/integration-shims/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/integration-shims", - "version": "7.55.2", + "version": "7.56.0", "description": "Shims for integrations in Sentry SDK.", "main": "build/cjs/index.js", "module": "build/esm/index.js", @@ -34,7 +34,7 @@ "url": "https://github.com/getsentry/sentry-javascript/issues" }, "dependencies": { - "@sentry/types": "7.55.2" + "@sentry/types": "7.56.0" }, "engines": { "node": ">=12" diff --git a/packages/integrations/package.json b/packages/integrations/package.json index 6077048b5bfc..13e1f5a04343 100644 --- a/packages/integrations/package.json +++ b/packages/integrations/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/integrations", - "version": "7.55.2", + "version": "7.56.0", "description": "Pluggable integrations that can be used to enhance JS SDKs", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/integrations", @@ -16,13 +16,13 @@ "module": "build/npm/esm/index.js", "types": "build/npm/types/index.d.ts", "dependencies": { - "@sentry/types": "7.55.2", - "@sentry/utils": "7.55.2", + "@sentry/types": "7.56.0", + "@sentry/utils": "7.56.0", "localforage": "^1.8.1", "tslib": "^1.9.3" }, "devDependencies": { - "@sentry/browser": "7.55.2", + "@sentry/browser": "7.56.0", "chai": "^4.1.2" }, "scripts": { diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index 69c4482a065e..69b24dcfbb44 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/nextjs", - "version": "7.55.2", + "version": "7.56.0", "description": "Official Sentry SDK for Next.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/nextjs", @@ -18,12 +18,12 @@ }, "dependencies": { "@rollup/plugin-commonjs": "24.0.0", - "@sentry/core": "7.55.2", - "@sentry/integrations": "7.55.2", - "@sentry/node": "7.55.2", - "@sentry/react": "7.55.2", - "@sentry/types": "7.55.2", - "@sentry/utils": "7.55.2", + "@sentry/core": "7.56.0", + "@sentry/integrations": "7.56.0", + "@sentry/node": "7.56.0", + "@sentry/react": "7.56.0", + "@sentry/types": "7.56.0", + "@sentry/utils": "7.56.0", "@sentry/webpack-plugin": "1.20.0", "chalk": "3.0.0", "rollup": "2.78.0", diff --git a/packages/node-integration-tests/package.json b/packages/node-integration-tests/package.json index bea1e5f93b8c..2c6d3fc29ff1 100644 --- a/packages/node-integration-tests/package.json +++ b/packages/node-integration-tests/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/node-integration-tests", - "version": "7.55.2", + "version": "7.56.0", "license": "MIT", "engines": { "node": ">=10" diff --git a/packages/node/package.json b/packages/node/package.json index 5443313cccaf..c24c033ab70f 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/node", - "version": "7.55.2", + "version": "7.56.0", "description": "Official Sentry SDK for Node.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/node", @@ -16,10 +16,10 @@ "access": "public" }, "dependencies": { - "@sentry-internal/tracing": "7.55.2", - "@sentry/core": "7.55.2", - "@sentry/types": "7.55.2", - "@sentry/utils": "7.55.2", + "@sentry-internal/tracing": "7.56.0", + "@sentry/core": "7.56.0", + "@sentry/types": "7.56.0", + "@sentry/utils": "7.56.0", "cookie": "^0.4.1", "https-proxy-agent": "^5.0.0", "lru_map": "^0.3.3", diff --git a/packages/opentelemetry-node/package.json b/packages/opentelemetry-node/package.json index b0a3cb75dc59..4cd6cde98e76 100644 --- a/packages/opentelemetry-node/package.json +++ b/packages/opentelemetry-node/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/opentelemetry-node", - "version": "7.55.2", + "version": "7.56.0", "description": "Official Sentry SDK for OpenTelemetry Node.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/opentelemetry-node", @@ -16,9 +16,9 @@ "access": "public" }, "dependencies": { - "@sentry/core": "7.55.2", - "@sentry/types": "7.55.2", - "@sentry/utils": "7.55.2" + "@sentry/core": "7.56.0", + "@sentry/types": "7.56.0", + "@sentry/utils": "7.56.0" }, "peerDependencies": { "@opentelemetry/api": "1.x", @@ -32,7 +32,7 @@ "@opentelemetry/sdk-trace-base": "^1.7.0", "@opentelemetry/sdk-trace-node": "^1.7.0", "@opentelemetry/semantic-conventions": "^1.7.0", - "@sentry/node": "7.55.2" + "@sentry/node": "7.56.0" }, "scripts": { "build": "run-p build:transpile build:types", diff --git a/packages/overhead-metrics/package.json b/packages/overhead-metrics/package.json index 621b2806f7bc..910f6b0e001c 100644 --- a/packages/overhead-metrics/package.json +++ b/packages/overhead-metrics/package.json @@ -1,6 +1,6 @@ { "private": true, - "version": "7.55.2", + "version": "7.56.0", "name": "@sentry-internal/overhead-metrics", "main": "index.js", "author": "Sentry", diff --git a/packages/react/package.json b/packages/react/package.json index a89156197031..b0f360019949 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/react", - "version": "7.55.2", + "version": "7.56.0", "description": "Official Sentry SDK for React.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/react", @@ -16,9 +16,9 @@ "access": "public" }, "dependencies": { - "@sentry/browser": "7.55.2", - "@sentry/types": "7.55.2", - "@sentry/utils": "7.55.2", + "@sentry/browser": "7.56.0", + "@sentry/types": "7.56.0", + "@sentry/utils": "7.56.0", "hoist-non-react-statics": "^3.3.2", "tslib": "^1.9.3" }, diff --git a/packages/remix/package.json b/packages/remix/package.json index 8e389d438a10..e964b237149c 100644 --- a/packages/remix/package.json +++ b/packages/remix/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/remix", - "version": "7.55.2", + "version": "7.56.0", "description": "Official Sentry SDK for Remix", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/remix", @@ -21,11 +21,11 @@ }, "dependencies": { "@sentry/cli": "2.2.0", - "@sentry/core": "7.55.2", - "@sentry/node": "7.55.2", - "@sentry/react": "7.55.2", - "@sentry/types": "7.55.2", - "@sentry/utils": "7.55.2", + "@sentry/core": "7.56.0", + "@sentry/node": "7.56.0", + "@sentry/react": "7.56.0", + "@sentry/types": "7.56.0", + "@sentry/utils": "7.56.0", "tslib": "^1.9.3", "yargs": "^17.6.0" }, diff --git a/packages/replay-worker/package.json b/packages/replay-worker/package.json index 8bbe38d8a33d..3641e152f07e 100644 --- a/packages/replay-worker/package.json +++ b/packages/replay-worker/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/replay-worker", - "version": "7.55.2", + "version": "7.56.0", "description": "Worker for @sentry/replay", "main": "build/npm/esm/index.js", "module": "build/npm/esm/index.js", diff --git a/packages/replay/package.json b/packages/replay/package.json index d54e53bc59af..f3b1cc69d2b9 100644 --- a/packages/replay/package.json +++ b/packages/replay/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/replay", - "version": "7.55.2", + "version": "7.56.0", "description": "User replays for Sentry", "main": "build/npm/cjs/index.js", "module": "build/npm/esm/index.js", @@ -44,16 +44,16 @@ "homepage": "https://docs.sentry.io/platforms/javascript/session-replay/", "devDependencies": { "@babel/core": "^7.17.5", - "@sentry-internal/replay-worker": "7.55.2", + "@sentry-internal/replay-worker": "7.56.0", "@sentry-internal/rrweb": "1.108.0", "@sentry-internal/rrweb-snapshot": "1.108.0", "jsdom-worker": "^0.2.1", "tslib": "^1.9.3" }, "dependencies": { - "@sentry/core": "7.55.2", - "@sentry/types": "7.55.2", - "@sentry/utils": "7.55.2" + "@sentry/core": "7.56.0", + "@sentry/types": "7.56.0", + "@sentry/utils": "7.56.0" }, "engines": { "node": ">=12" diff --git a/packages/serverless/package.json b/packages/serverless/package.json index 6bb1777c34d4..167e3ac936f7 100644 --- a/packages/serverless/package.json +++ b/packages/serverless/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/serverless", - "version": "7.55.2", + "version": "7.56.0", "description": "Official Sentry SDK for various serverless solutions", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/serverless", @@ -16,10 +16,10 @@ "access": "public" }, "dependencies": { - "@sentry/core": "7.55.2", - "@sentry/node": "7.55.2", - "@sentry/types": "7.55.2", - "@sentry/utils": "7.55.2", + "@sentry/core": "7.56.0", + "@sentry/node": "7.56.0", + "@sentry/types": "7.56.0", + "@sentry/utils": "7.56.0", "@types/aws-lambda": "^8.10.62", "@types/express": "^4.17.14", "tslib": "^1.9.3" diff --git a/packages/svelte/package.json b/packages/svelte/package.json index f9ef94fb1f08..1aa37e2101bf 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/svelte", - "version": "7.55.2", + "version": "7.56.0", "description": "Official Sentry SDK for Svelte", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/svelte", @@ -16,9 +16,9 @@ "access": "public" }, "dependencies": { - "@sentry/browser": "7.55.2", - "@sentry/types": "7.55.2", - "@sentry/utils": "7.55.2", + "@sentry/browser": "7.56.0", + "@sentry/types": "7.56.0", + "@sentry/utils": "7.56.0", "magic-string": "^0.30.0", "tslib": "^1.9.3" }, diff --git a/packages/sveltekit/package.json b/packages/sveltekit/package.json index da1015dcf2c5..ef3a0ccbadc5 100644 --- a/packages/sveltekit/package.json +++ b/packages/sveltekit/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/sveltekit", - "version": "7.55.2", + "version": "7.56.0", "description": "Official Sentry SDK for SvelteKit", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/sveltekit", @@ -20,13 +20,13 @@ "@sveltejs/kit": "1.x" }, "dependencies": { - "@sentry-internal/tracing": "7.55.2", - "@sentry/core": "7.55.2", - "@sentry/integrations": "7.55.2", - "@sentry/node": "7.55.2", - "@sentry/svelte": "7.55.2", - "@sentry/types": "7.55.2", - "@sentry/utils": "7.55.2", + "@sentry-internal/tracing": "7.56.0", + "@sentry/core": "7.56.0", + "@sentry/integrations": "7.56.0", + "@sentry/node": "7.56.0", + "@sentry/svelte": "7.56.0", + "@sentry/types": "7.56.0", + "@sentry/utils": "7.56.0", "@sentry/vite-plugin": "^0.6.0", "magicast": "0.2.8", "sorcery": "0.11.0" diff --git a/packages/tracing-internal/package.json b/packages/tracing-internal/package.json index 614fb4f51a87..9c77cb6c6334 100644 --- a/packages/tracing-internal/package.json +++ b/packages/tracing-internal/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/tracing", - "version": "7.55.2", + "version": "7.56.0", "description": "Sentry Internal Tracing Package", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/tracing-internal", @@ -16,9 +16,9 @@ "access": "public" }, "dependencies": { - "@sentry/core": "7.55.2", - "@sentry/types": "7.55.2", - "@sentry/utils": "7.55.2", + "@sentry/core": "7.56.0", + "@sentry/types": "7.56.0", + "@sentry/utils": "7.56.0", "tslib": "^1.9.3" }, "devDependencies": { diff --git a/packages/tracing/package.json b/packages/tracing/package.json index f0dabb89c27d..c177599cdcfe 100644 --- a/packages/tracing/package.json +++ b/packages/tracing/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/tracing", - "version": "7.55.2", + "version": "7.56.0", "description": "Sentry Performance Monitoring Package", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/tracing", @@ -16,14 +16,14 @@ "access": "public" }, "dependencies": { - "@sentry-internal/tracing": "7.55.2" + "@sentry-internal/tracing": "7.56.0" }, "devDependencies": { - "@sentry-internal/integration-shims": "7.55.2", - "@sentry/browser": "7.55.2", - "@sentry/core": "7.55.2", - "@sentry/types": "7.55.2", - "@sentry/utils": "7.55.2", + "@sentry-internal/integration-shims": "7.56.0", + "@sentry/browser": "7.56.0", + "@sentry/core": "7.56.0", + "@sentry/types": "7.56.0", + "@sentry/utils": "7.56.0", "@types/express": "^4.17.14" }, "scripts": { diff --git a/packages/types/package.json b/packages/types/package.json index 5a01717aac7d..736924037fc0 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/types", - "version": "7.55.2", + "version": "7.56.0", "description": "Types for all Sentry JavaScript SDKs", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/types", diff --git a/packages/typescript/package.json b/packages/typescript/package.json index d245845a227a..44c0379492fc 100644 --- a/packages/typescript/package.json +++ b/packages/typescript/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/typescript", - "version": "7.55.2", + "version": "7.56.0", "description": "Typescript configuration used at Sentry", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/typescript", diff --git a/packages/utils/package.json b/packages/utils/package.json index 88c8c4fcb4f3..625cf7652e13 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/utils", - "version": "7.55.2", + "version": "7.56.0", "description": "Utilities for all Sentry JavaScript SDKs", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/utils", @@ -16,7 +16,7 @@ "access": "public" }, "dependencies": { - "@sentry/types": "7.55.2", + "@sentry/types": "7.56.0", "tslib": "^1.9.3" }, "devDependencies": { diff --git a/packages/vue/package.json b/packages/vue/package.json index 236435b93e01..b82ca6ae4e61 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/vue", - "version": "7.55.2", + "version": "7.56.0", "description": "Official Sentry SDK for Vue.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/vue", @@ -16,10 +16,10 @@ "access": "public" }, "dependencies": { - "@sentry/browser": "7.55.2", - "@sentry/core": "7.55.2", - "@sentry/types": "7.55.2", - "@sentry/utils": "7.55.2", + "@sentry/browser": "7.56.0", + "@sentry/core": "7.56.0", + "@sentry/types": "7.56.0", + "@sentry/utils": "7.56.0", "tslib": "^1.9.3" }, "peerDependencies": { diff --git a/packages/wasm/package.json b/packages/wasm/package.json index 6a9275e9a1bb..fcd2bef8a637 100644 --- a/packages/wasm/package.json +++ b/packages/wasm/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/wasm", - "version": "7.55.2", + "version": "7.56.0", "description": "Support for WASM.", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/wasm", @@ -16,9 +16,9 @@ "access": "public" }, "dependencies": { - "@sentry/browser": "7.55.2", - "@sentry/types": "7.55.2", - "@sentry/utils": "7.55.2", + "@sentry/browser": "7.56.0", + "@sentry/types": "7.56.0", + "@sentry/utils": "7.56.0", "tslib": "^1.9.3" }, "scripts": { From df4f4ab8734192cef2f04e2b672c76274da1dfdb Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Mon, 19 Jun 2023 10:49:47 -0400 Subject: [PATCH 3/4] feat(replay): Do not capture replays < 5 seconds (GA) (#8277) --- .../suites/replay/bufferMode/test.ts | 19 +- .../suites/replay/fileInput/test.ts | 7 +- .../largeMutations/defaultOptions/test.ts | 4 - .../largeMutations/mutationLimit/test.ts | 3 - .../suites/replay/slowClick/mutation/test.ts | 94 +- .../suites/replay/throttleBreadcrumbs/test.ts | 13 +- .../tests/fixtures/ReplayRecordingData.ts | 22 - packages/replay/src/replay.ts | 7 + packages/replay/src/types/replay.ts | 2 +- .../replay/src/util/handleRecordingEmit.ts | 33 +- .../coreHandlers/handleAfterSendEvent.test.ts | 6 +- .../errorSampleRate-delayFlush.test.ts | 865 ------------------ .../test/integration/errorSampleRate.test.ts | 217 +++-- 13 files changed, 222 insertions(+), 1070 deletions(-) delete mode 100644 packages/replay/test/integration/errorSampleRate-delayFlush.test.ts diff --git a/packages/browser-integration-tests/suites/replay/bufferMode/test.ts b/packages/browser-integration-tests/suites/replay/bufferMode/test.ts index a9a9dbbe86e5..4785c1a5b158 100644 --- a/packages/browser-integration-tests/suites/replay/bufferMode/test.ts +++ b/packages/browser-integration-tests/suites/replay/bufferMode/test.ts @@ -25,7 +25,6 @@ sentryTest( let errorEventId: string | undefined; const reqPromise0 = waitForReplayRequest(page, 0); const reqPromise1 = waitForReplayRequest(page, 1); - const reqPromise2 = waitForReplayRequest(page, 2); const reqErrorPromise = waitForErrorRequest(page); await page.route('https://dsn.ingest.sentry.io/**/*', route => { @@ -101,8 +100,7 @@ sentryTest( // Switches to session mode and then goes to background const req1 = await reqPromise1; - const req2 = await reqPromise2; - expect(callsToSentry).toBeGreaterThanOrEqual(5); + expect(callsToSentry).toBeGreaterThanOrEqual(4); const event0 = getReplayEvent(req0); const content0 = getReplayRecordingContent(req0); @@ -110,9 +108,6 @@ sentryTest( const event1 = getReplayEvent(req1); const content1 = getReplayRecordingContent(req1); - const event2 = getReplayEvent(req2); - const content2 = getReplayRecordingContent(req2); - expect(event0).toEqual( getExpectedReplayEvent({ error_ids: [errorEventId!], @@ -157,17 +152,7 @@ sentryTest( // From switching to session mode expect(content1.fullSnapshots).toHaveLength(1); - - expect(event2).toEqual( - getExpectedReplayEvent({ - replay_type: 'buffer', // although we're in session mode, we still send 'buffer' as replay_type - segment_id: 2, - urls: [], - }), - ); - - expect(content2.fullSnapshots).toHaveLength(0); - expect(content2.breadcrumbs).toEqual(expect.arrayContaining([expectedClickBreadcrumb])); + expect(content1.breadcrumbs).toEqual(expect.arrayContaining([expectedClickBreadcrumb])); }, ); diff --git a/packages/browser-integration-tests/suites/replay/fileInput/test.ts b/packages/browser-integration-tests/suites/replay/fileInput/test.ts index 685c626ec470..e0827538ba56 100644 --- a/packages/browser-integration-tests/suites/replay/fileInput/test.ts +++ b/packages/browser-integration-tests/suites/replay/fileInput/test.ts @@ -25,7 +25,6 @@ sentryTest( } const reqPromise0 = waitForReplayRequest(page, 0); - const reqPromise1 = waitForReplayRequest(page, 1); await page.route('https://dsn.ingest.sentry.io/**/*', route => { return route.fulfill({ @@ -39,7 +38,7 @@ sentryTest( await page.goto(url); - await reqPromise0; + const res = await reqPromise0; await page.setInputFiles('#file-input', { name: 'file.csv', @@ -49,9 +48,7 @@ sentryTest( await forceFlushReplay(); - const res1 = await reqPromise1; - - const snapshots = getIncrementalRecordingSnapshots(res1).filter(isInputMutation); + const snapshots = getIncrementalRecordingSnapshots(res).filter(isInputMutation); expect(snapshots).toEqual([]); }, diff --git a/packages/browser-integration-tests/suites/replay/largeMutations/defaultOptions/test.ts b/packages/browser-integration-tests/suites/replay/largeMutations/defaultOptions/test.ts index 29d0f3ada164..c0d8e8234da8 100644 --- a/packages/browser-integration-tests/suites/replay/largeMutations/defaultOptions/test.ts +++ b/packages/browser-integration-tests/suites/replay/largeMutations/defaultOptions/test.ts @@ -11,7 +11,6 @@ sentryTest( } const reqPromise0 = waitForReplayRequest(page, 0); - const reqPromise0b = waitForReplayRequest(page, 1); await page.route('https://dsn.ingest.sentry.io/**/*', route => { return route.fulfill({ @@ -24,10 +23,7 @@ sentryTest( const url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); - await forceFlushReplay(); const res0 = await reqPromise0; - await reqPromise0b; - // A second request is sent right after initial snapshot, we want to wait for that to settle before we continue const reqPromise1 = waitForReplayRequest(page); diff --git a/packages/browser-integration-tests/suites/replay/largeMutations/mutationLimit/test.ts b/packages/browser-integration-tests/suites/replay/largeMutations/mutationLimit/test.ts index 84f0113263d7..b826daafe6b4 100644 --- a/packages/browser-integration-tests/suites/replay/largeMutations/mutationLimit/test.ts +++ b/packages/browser-integration-tests/suites/replay/largeMutations/mutationLimit/test.ts @@ -16,7 +16,6 @@ sentryTest( } const reqPromise0 = waitForReplayRequest(page, 0); - const reqPromise0b = waitForReplayRequest(page, 1); await page.route('https://dsn.ingest.sentry.io/**/*', route => { return route.fulfill({ @@ -30,8 +29,6 @@ sentryTest( await page.goto(url); const res0 = await reqPromise0; - await reqPromise0b; - // A second request is sent right after initial snapshot, we want to wait for that to settle before we continue const reqPromise1 = waitForReplayRequest(page); diff --git a/packages/browser-integration-tests/suites/replay/slowClick/mutation/test.ts b/packages/browser-integration-tests/suites/replay/slowClick/mutation/test.ts index deb394ebac2d..bab50e12938c 100644 --- a/packages/browser-integration-tests/suites/replay/slowClick/mutation/test.ts +++ b/packages/browser-integration-tests/suites/replay/slowClick/mutation/test.ts @@ -3,7 +3,7 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; import { getCustomRecordingEvents, shouldSkipReplayTest, waitForReplayRequest } from '../../../../utils/replayHelpers'; -sentryTest('mutation after threshold results in slow click', async ({ getLocalTestUrl, page }) => { +sentryTest('mutation after threshold results in slow click', async ({ forceFlushReplay, getLocalTestUrl, page }) => { if (shouldSkipReplayTest()) { sentryTest.skip(); } @@ -21,6 +21,7 @@ sentryTest('mutation after threshold results in slow click', async ({ getLocalTe const url = await getLocalTestUrl({ testDir: __dirname }); await page.goto(url); + await forceFlushReplay(); await reqPromise0; const reqPromise1 = waitForReplayRequest(page, (event, res) => { @@ -125,59 +126,63 @@ sentryTest('multiple clicks are counted', async ({ getLocalTestUrl, page }) => { expect(slowClickBreadcrumbs[0]?.data?.timeAfterClickMs).toBeLessThan(3100); }); -sentryTest('immediate mutation does not trigger slow click', async ({ browserName, getLocalTestUrl, page }) => { - // This test seems to only be flakey on firefox - if (shouldSkipReplayTest() || ['firefox'].includes(browserName)) { - sentryTest.skip(); - } - - const reqPromise0 = waitForReplayRequest(page, 0); - - await page.route('https://dsn.ingest.sentry.io/**/*', route => { - return route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ id: 'test-id' }), +sentryTest( + 'immediate mutation does not trigger slow click', + async ({ forceFlushReplay, browserName, getLocalTestUrl, page }) => { + // This test seems to only be flakey on firefox + if (shouldSkipReplayTest() || ['firefox'].includes(browserName)) { + sentryTest.skip(); + } + + const reqPromise0 = waitForReplayRequest(page, 0); + + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ id: 'test-id' }), + }); }); - }); - const url = await getLocalTestUrl({ testDir: __dirname }); + const url = await getLocalTestUrl({ testDir: __dirname }); - await page.goto(url); - await reqPromise0; + await page.goto(url); + await forceFlushReplay(); + await reqPromise0; - const reqPromise1 = waitForReplayRequest(page, (event, res) => { - const { breadcrumbs } = getCustomRecordingEvents(res); + const reqPromise1 = waitForReplayRequest(page, (event, res) => { + const { breadcrumbs } = getCustomRecordingEvents(res); - return breadcrumbs.some(breadcrumb => breadcrumb.category === 'ui.click'); - }); - - await page.click('#mutationButtonImmediately'); - - const { breadcrumbs } = getCustomRecordingEvents(await reqPromise1); + return breadcrumbs.some(breadcrumb => breadcrumb.category === 'ui.click'); + }); - expect(breadcrumbs).toEqual([ - { - category: 'ui.click', - data: { - node: { - attributes: { - id: 'mutationButtonImmediately', + await page.click('#mutationButtonImmediately'); + + const { breadcrumbs } = getCustomRecordingEvents(await reqPromise1); + + expect(breadcrumbs).toEqual([ + { + category: 'ui.click', + data: { + node: { + attributes: { + id: 'mutationButtonImmediately', + }, + id: expect.any(Number), + tagName: 'button', + textContent: '******* ******** ***********', }, - id: expect.any(Number), - tagName: 'button', - textContent: '******* ******** ***********', + nodeId: expect.any(Number), }, - nodeId: expect.any(Number), + message: 'body > button#mutationButtonImmediately', + timestamp: expect.any(Number), + type: 'default', }, - message: 'body > button#mutationButtonImmediately', - timestamp: expect.any(Number), - type: 'default', - }, - ]); -}); + ]); + }, +); -sentryTest('inline click handler does not trigger slow click', async ({ getLocalTestUrl, page }) => { +sentryTest('inline click handler does not trigger slow click', async ({ forceFlushReplay, getLocalTestUrl, page }) => { if (shouldSkipReplayTest()) { sentryTest.skip(); } @@ -195,6 +200,7 @@ sentryTest('inline click handler does not trigger slow click', async ({ getLocal const url = await getLocalTestUrl({ testDir: __dirname }); await page.goto(url); + await forceFlushReplay(); await reqPromise0; const reqPromise1 = waitForReplayRequest(page, (event, res) => { diff --git a/packages/browser-integration-tests/suites/replay/throttleBreadcrumbs/test.ts b/packages/browser-integration-tests/suites/replay/throttleBreadcrumbs/test.ts index 17f4210624a0..e025c90a77e0 100644 --- a/packages/browser-integration-tests/suites/replay/throttleBreadcrumbs/test.ts +++ b/packages/browser-integration-tests/suites/replay/throttleBreadcrumbs/test.ts @@ -26,16 +26,19 @@ sentryTest( const url = await getLocalTestUrl({ testDir: __dirname }); await page.goto(url); - await reqPromise0; + await forceFlushReplay(); + const res0 = getCustomRecordingEvents(await reqPromise0); await page.click('[data-console]'); await forceFlushReplay(); - const { breadcrumbs } = getCustomRecordingEvents(await reqPromise1); + const res1 = getCustomRecordingEvents(await reqPromise1); - // 1 click breadcrumb + 1 throttled breadcrumb is why console logs are less - // than throttle limit - expect(breadcrumbs.length).toBe(THROTTLE_LIMIT); + const breadcrumbs = [...res0.breadcrumbs, ...res1.breadcrumbs]; + const spans = [...res0.performanceSpans, ...res1.performanceSpans]; expect(breadcrumbs.filter(breadcrumb => breadcrumb.category === 'replay.throttled').length).toBe(1); + // replay.throttled breadcrumb does *not* use the throttledAddEvent as we + // alwants want that breadcrumb to be present in replay + expect(breadcrumbs.length + spans.length).toBe(THROTTLE_LIMIT + 1); }, ); diff --git a/packages/e2e-tests/test-applications/standard-frontend-react/tests/fixtures/ReplayRecordingData.ts b/packages/e2e-tests/test-applications/standard-frontend-react/tests/fixtures/ReplayRecordingData.ts index a22694a64304..0da2e1b2e327 100644 --- a/packages/e2e-tests/test-applications/standard-frontend-react/tests/fixtures/ReplayRecordingData.ts +++ b/packages/e2e-tests/test-applications/standard-frontend-react/tests/fixtures/ReplayRecordingData.ts @@ -95,26 +95,6 @@ export const ReplayRecordingData = [ }, timestamp: expect.any(Number), }, - { - type: 5, - timestamp: expect.any(Number), - data: { - tag: 'performanceSpan', - payload: { - op: 'memory', - description: 'memory', - startTimestamp: expect.any(Number), - endTimestamp: expect.any(Number), - data: { - memory: { - jsHeapSizeLimit: expect.any(Number), - totalJSHeapSize: expect.any(Number), - usedJSHeapSize: expect.any(Number), - }, - }, - }, - }, - }, { type: 3, data: { @@ -155,8 +135,6 @@ export const ReplayRecordingData = [ data: { source: 5, text: 'Capture Exception', isChecked: false, id: 16 }, timestamp: expect.any(Number), }, - ], - [ { type: 5, timestamp: expect.any(Number), diff --git a/packages/replay/src/replay.ts b/packages/replay/src/replay.ts index acb2980e608c..8fab410a0c34 100644 --- a/packages/replay/src/replay.ts +++ b/packages/replay/src/replay.ts @@ -547,6 +547,13 @@ export class ReplayContainer implements ReplayContainerInterface { return this.flushImmediate(); } + /** + * Flush using debounce flush + */ + public flush(): Promise { + return this._debouncedFlush() as Promise; + } + /** * Always flush via `_debouncedFlush` so that we do not have flushes triggered * from calling both `flush` and `_debouncedFlush`. Otherwise, there could be diff --git a/packages/replay/src/types/replay.ts b/packages/replay/src/types/replay.ts index f058b2c9011a..e2cfddd0f525 100644 --- a/packages/replay/src/types/replay.ts +++ b/packages/replay/src/types/replay.ts @@ -194,7 +194,6 @@ export interface ReplayPluginOptions extends ReplayNetworkOptions { _experiments: Partial<{ captureExceptions: boolean; traceInternals: boolean; - delayFlushOnCheckout: number; }>; } @@ -438,6 +437,7 @@ export interface ReplayContainer { stopRecording(): boolean; sendBufferedReplayOrFlush(options?: SendBufferedReplayOptions): Promise; conditionalFlush(): Promise; + flush(): Promise; flushImmediate(): Promise; cancelFlush(): void; triggerUserActivity(): void; diff --git a/packages/replay/src/util/handleRecordingEmit.ts b/packages/replay/src/util/handleRecordingEmit.ts index cc7c87afed48..987f589412ed 100644 --- a/packages/replay/src/util/handleRecordingEmit.ts +++ b/packages/replay/src/util/handleRecordingEmit.ts @@ -80,41 +80,12 @@ export function getHandleRecordingEmit(replay: ReplayContainer): RecordingEmitCa } } - const options = replay.getOptions(); - - // TODO: We want this as an experiment so that we can test - // internally and create metrics before making this the default - if (options._experiments.delayFlushOnCheckout) { + if (replay.recordingMode === 'session') { // If the full snapshot is due to an initial load, we will not have // a previous session ID. In this case, we want to buffer events // for a set amount of time before flushing. This can help avoid // capturing replays of users that immediately close the window. - // TODO: We should check `recordingMode` here and do nothing if it's - // buffer, instead of checking inside of timeout, this will make our - // tests a bit cleaner as we will need to wait on the delay in order to - // do nothing. - setTimeout(() => replay.conditionalFlush(), options._experiments.delayFlushOnCheckout); - - // Cancel any previously debounced flushes to ensure there are no [near] - // simultaneous flushes happening. The latter request should be - // insignificant in this case, so wait for additional user interaction to - // trigger a new flush. - // - // This can happen because there's no guarantee that a recording event - // happens first. e.g. a mouse click can happen and trigger a debounced - // flush before the checkout. - replay.cancelFlush(); - - return true; - } - - // Flush immediately so that we do not miss the first segment, otherwise - // it can prevent loading on the UI. This will cause an increase in short - // replays (e.g. opening and closing a tab quickly), but these can be - // filtered on the UI. - if (replay.recordingMode === 'session') { - // We want to ensure the worker is ready, as otherwise we'd always send the first event uncompressed - void replay.flushImmediate(); + void replay.flush(); } return true; diff --git a/packages/replay/test/integration/coreHandlers/handleAfterSendEvent.test.ts b/packages/replay/test/integration/coreHandlers/handleAfterSendEvent.test.ts index 1e59a4f7eef0..cd367a2d04f5 100644 --- a/packages/replay/test/integration/coreHandlers/handleAfterSendEvent.test.ts +++ b/packages/replay/test/integration/coreHandlers/handleAfterSendEvent.test.ts @@ -148,9 +148,13 @@ describe('Integration | coreHandlers | handleAfterSendEvent', () => { jest.runAllTimers(); await new Promise(process.nextTick); - // Send twice, one for the error & one right after for the session conversion + expect(mockSend).toHaveBeenCalledTimes(1); + + jest.runAllTimers(); + await new Promise(process.nextTick); expect(mockSend).toHaveBeenCalledTimes(2); + // This is removed now, because it has been converted to a "session" session expect(Array.from(replay.getContext().errorIds)).toEqual([]); expect(replay.isEnabled()).toBe(true); diff --git a/packages/replay/test/integration/errorSampleRate-delayFlush.test.ts b/packages/replay/test/integration/errorSampleRate-delayFlush.test.ts deleted file mode 100644 index f691d8e953c1..000000000000 --- a/packages/replay/test/integration/errorSampleRate-delayFlush.test.ts +++ /dev/null @@ -1,865 +0,0 @@ -import { captureException, getCurrentHub } from '@sentry/core'; - -import { - BUFFER_CHECKOUT_TIME, - DEFAULT_FLUSH_MIN_DELAY, - MAX_SESSION_LIFE, - REPLAY_SESSION_KEY, - SESSION_IDLE_EXPIRE_DURATION, - WINDOW, -} from '../../src/constants'; -import type { ReplayContainer } from '../../src/replay'; -import { clearSession } from '../../src/session/clearSession'; -import { addEvent } from '../../src/util/addEvent'; -import { createOptionsEvent } from '../../src/util/handleRecordingEmit'; -import { PerformanceEntryResource } from '../fixtures/performanceEntry/resource'; -import type { RecordMock } from '../index'; -import { BASE_TIMESTAMP } from '../index'; -import { resetSdkMock } from '../mocks/resetSdkMock'; -import type { DomHandler } from '../types'; -import { useFakeTimers } from '../utils/use-fake-timers'; - -useFakeTimers(); - -async function advanceTimers(time: number) { - jest.advanceTimersByTime(time); - await new Promise(process.nextTick); -} - -async function waitForBufferFlush() { - await new Promise(process.nextTick); - await new Promise(process.nextTick); -} - -async function waitForFlush() { - await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); -} - -describe('Integration | errorSampleRate with delayed flush', () => { - let replay: ReplayContainer; - let mockRecord: RecordMock; - let domHandler: DomHandler; - - beforeEach(async () => { - ({ mockRecord, domHandler, replay } = await resetSdkMock({ - replayOptions: { - stickySession: true, - _experiments: { - delayFlushOnCheckout: DEFAULT_FLUSH_MIN_DELAY, - }, - }, - sentryOptions: { - replaysSessionSampleRate: 0.0, - replaysOnErrorSampleRate: 1.0, - }, - })); - }); - - afterEach(async () => { - clearSession(replay); - replay.stop(); - }); - - it('uploads a replay when `Sentry.captureException` is called and continues recording', async () => { - const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 3 }; - mockRecord._emitter(TEST_EVENT); - const optionsEvent = createOptionsEvent(replay); - - expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); - expect(replay).not.toHaveLastSentReplay(); - - // Does not capture on mouse click - domHandler({ - name: 'click', - }); - jest.runAllTimers(); - await new Promise(process.nextTick); - expect(replay).not.toHaveLastSentReplay(); - - captureException(new Error('testing')); - - await waitForBufferFlush(); - - expect(replay).toHaveLastSentReplay({ - recordingPayloadHeader: { segment_id: 0 }, - replayEventPayload: expect.objectContaining({ - replay_type: 'buffer', - }), - recordingData: JSON.stringify([ - { data: { isCheckout: true }, timestamp: BASE_TIMESTAMP, type: 2 }, - optionsEvent, - TEST_EVENT, - { - type: 5, - timestamp: BASE_TIMESTAMP, - data: { - tag: 'breadcrumb', - payload: { - timestamp: BASE_TIMESTAMP / 1000, - type: 'default', - category: 'ui.click', - message: '', - data: {}, - }, - }, - }, - ]), - }); - - await waitForFlush(); - - // This is from when we stop recording and start a session recording - expect(replay).toHaveLastSentReplay({ - recordingPayloadHeader: { segment_id: 1 }, - replayEventPayload: expect.objectContaining({ - replay_type: 'buffer', - }), - recordingData: JSON.stringify([{ data: { isCheckout: true }, timestamp: BASE_TIMESTAMP + 40, type: 2 }]), - }); - - jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY); - - // Check that click will get captured - domHandler({ - name: 'click', - }); - - await waitForFlush(); - - expect(replay).toHaveLastSentReplay({ - recordingData: JSON.stringify([ - { - type: 5, - timestamp: BASE_TIMESTAMP + 10000 + 80, - data: { - tag: 'breadcrumb', - payload: { - timestamp: (BASE_TIMESTAMP + 10000 + 80) / 1000, - type: 'default', - category: 'ui.click', - message: '', - data: {}, - }, - }, - }, - ]), - }); - }); - - it('manually flushes replay and does not continue to record', async () => { - const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 3 }; - mockRecord._emitter(TEST_EVENT); - const optionsEvent = createOptionsEvent(replay); - - expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); - expect(replay).not.toHaveLastSentReplay(); - - // Does not capture on mouse click - domHandler({ - name: 'click', - }); - jest.runAllTimers(); - await new Promise(process.nextTick); - expect(replay).not.toHaveLastSentReplay(); - - replay.sendBufferedReplayOrFlush({ continueRecording: false }); - - await waitForBufferFlush(); - - expect(replay).toHaveSentReplay({ - recordingPayloadHeader: { segment_id: 0 }, - replayEventPayload: expect.objectContaining({ - replay_type: 'buffer', - }), - recordingData: JSON.stringify([ - { data: { isCheckout: true }, timestamp: BASE_TIMESTAMP, type: 2 }, - optionsEvent, - TEST_EVENT, - { - type: 5, - timestamp: BASE_TIMESTAMP, - data: { - tag: 'breadcrumb', - payload: { - timestamp: BASE_TIMESTAMP / 1000, - type: 'default', - category: 'ui.click', - message: '', - data: {}, - }, - }, - }, - ]), - }); - - jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY); - // Check that click will not get captured - domHandler({ - name: 'click', - }); - - await waitForFlush(); - - // This is still the last replay sent since we passed `continueRecording: - // false`. - expect(replay).toHaveLastSentReplay({ - recordingPayloadHeader: { segment_id: 0 }, - replayEventPayload: expect.objectContaining({ - replay_type: 'buffer', - }), - recordingData: JSON.stringify([ - { data: { isCheckout: true }, timestamp: BASE_TIMESTAMP, type: 2 }, - optionsEvent, - TEST_EVENT, - { - type: 5, - timestamp: BASE_TIMESTAMP, - data: { - tag: 'breadcrumb', - payload: { - timestamp: BASE_TIMESTAMP / 1000, - type: 'default', - category: 'ui.click', - message: '', - data: {}, - }, - }, - }, - ]), - }); - }); - - // This tests a regression where we were calling flush indiscriminantly in `stop()` - it('does not upload a replay event if error is not sampled', async () => { - // We are trying to replicate the case where error rate is 0 and session - // rate is > 0, we can't set them both to 0 otherwise - // `_loadAndCheckSession` is not called when initializing the plugin. - replay.stop(); - replay['_options']['errorSampleRate'] = 0; - replay['_loadAndCheckSession'](); - - jest.runAllTimers(); - await new Promise(process.nextTick); - expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); - expect(replay).not.toHaveLastSentReplay(); - }); - - it('does not send a replay when triggering a full dom snapshot when document becomes visible after [SESSION_IDLE_EXPIRE_DURATION]ms', async () => { - Object.defineProperty(document, 'visibilityState', { - configurable: true, - get: function () { - return 'visible'; - }, - }); - - jest.advanceTimersByTime(SESSION_IDLE_EXPIRE_DURATION + 1); - - document.dispatchEvent(new Event('visibilitychange')); - - jest.runAllTimers(); - await new Promise(process.nextTick); - - expect(replay).not.toHaveLastSentReplay(); - }); - - it('does not send a replay if user hides the tab and comes back within 60 seconds', async () => { - Object.defineProperty(document, 'visibilityState', { - configurable: true, - get: function () { - return 'hidden'; - }, - }); - document.dispatchEvent(new Event('visibilitychange')); - - jest.runAllTimers(); - await new Promise(process.nextTick); - - expect(replay).not.toHaveLastSentReplay(); - - // User comes back before `SESSION_IDLE_EXPIRE_DURATION` elapses - jest.advanceTimersByTime(SESSION_IDLE_EXPIRE_DURATION - 100); - Object.defineProperty(document, 'visibilityState', { - configurable: true, - get: function () { - return 'visible'; - }, - }); - document.dispatchEvent(new Event('visibilitychange')); - - jest.runAllTimers(); - await new Promise(process.nextTick); - - expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); - expect(replay).not.toHaveLastSentReplay(); - }); - - it('does not upload a replay event when document becomes hidden', async () => { - Object.defineProperty(document, 'visibilityState', { - configurable: true, - get: function () { - return 'hidden'; - }, - }); - - // Pretend 5 seconds have passed - const ELAPSED = 5000; - jest.advanceTimersByTime(ELAPSED); - - const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 2 }; - addEvent(replay, TEST_EVENT); - - document.dispatchEvent(new Event('visibilitychange')); - - jest.runAllTimers(); - await new Promise(process.nextTick); - - expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); - expect(replay).not.toHaveLastSentReplay(); - }); - - it('does not upload a replay event if 5 seconds have elapsed since the last replay event occurred', async () => { - const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 3 }; - mockRecord._emitter(TEST_EVENT); - // Pretend 5 seconds have passed - const ELAPSED = 5000; - await advanceTimers(ELAPSED); - - expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); - - jest.runAllTimers(); - await new Promise(process.nextTick); - - expect(replay).not.toHaveLastSentReplay(); - }); - - it('does not upload a replay event if 15 seconds have elapsed since the last replay upload', async () => { - const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 3 }; - // Fire a new event every 4 seconds, 4 times - [...Array(4)].forEach(() => { - mockRecord._emitter(TEST_EVENT); - jest.advanceTimersByTime(4000); - }); - - // We are at time = +16seconds now (relative to BASE_TIMESTAMP) - // The next event should cause an upload immediately - mockRecord._emitter(TEST_EVENT); - await new Promise(process.nextTick); - - expect(replay).not.toHaveLastSentReplay(); - - // There should also not be another attempt at an upload 5 seconds after the last replay event - await waitForFlush(); - expect(replay).not.toHaveLastSentReplay(); - - // Let's make sure it continues to work - mockRecord._emitter(TEST_EVENT); - await waitForFlush(); - jest.runAllTimers(); - await new Promise(process.nextTick); - expect(replay).not.toHaveLastSentReplay(); - }); - - // When the error session records as a normal session, we want to stop - // recording after the session ends. Otherwise, we get into a state where the - // new session is a session type replay (this could conflict with the session - // sample rate of 0.0), or an error session that has no errors. Instead we - // simply stop the session replay completely and wait for a new page load to - // resample. - it.each([ - ['MAX_SESSION_LIFE', MAX_SESSION_LIFE], - ['SESSION_IDLE_DURATION', SESSION_IDLE_EXPIRE_DURATION], - ])( - 'stops replay if session had an error and exceeds %s and does not start a new session thereafter', - async (_label, waitTime) => { - expect(replay.session?.shouldRefresh).toBe(true); - - captureException(new Error('testing')); - - await waitForBufferFlush(); - - expect(replay).toHaveLastSentReplay({ - recordingPayloadHeader: { segment_id: 0 }, - replayEventPayload: expect.objectContaining({ - replay_type: 'buffer', - }), - }); - - await waitForFlush(); - - // segment_id is 1 because it sends twice on error - expect(replay).toHaveLastSentReplay({ - recordingPayloadHeader: { segment_id: 1 }, - replayEventPayload: expect.objectContaining({ - replay_type: 'buffer', - }), - }); - expect(replay.session?.shouldRefresh).toBe(false); - - // Idle for given time - jest.advanceTimersByTime(waitTime + 1); - await new Promise(process.nextTick); - - const TEST_EVENT = { - data: { name: 'lost event' }, - timestamp: BASE_TIMESTAMP, - type: 3, - }; - mockRecord._emitter(TEST_EVENT); - - jest.runAllTimers(); - await new Promise(process.nextTick); - - // We stop recording after 15 minutes of inactivity in error mode - - // still no new replay sent - expect(replay).toHaveLastSentReplay({ - recordingPayloadHeader: { segment_id: 1 }, - replayEventPayload: expect.objectContaining({ - replay_type: 'buffer', - }), - }); - - expect(replay.isEnabled()).toBe(false); - - domHandler({ - name: 'click', - }); - - // Remains disabled! - expect(replay.isEnabled()).toBe(false); - }, - ); - - it.each([ - ['MAX_SESSION_LIFE', MAX_SESSION_LIFE], - ['SESSION_IDLE_EXPIRE_DURATION', SESSION_IDLE_EXPIRE_DURATION], - ])('continues buffering replay if session had no error and exceeds %s', async (_label, waitTime) => { - expect(replay).not.toHaveLastSentReplay(); - - // Idle for given time - jest.advanceTimersByTime(waitTime + 1); - await new Promise(process.nextTick); - - const TEST_EVENT = { - data: { name: 'lost event' }, - timestamp: BASE_TIMESTAMP, - type: 3, - }; - mockRecord._emitter(TEST_EVENT); - - jest.runAllTimers(); - await new Promise(process.nextTick); - - // still no new replay sent - expect(replay).not.toHaveLastSentReplay(); - - expect(replay.isEnabled()).toBe(true); - expect(replay.isPaused()).toBe(false); - expect(replay.recordingMode).toBe('buffer'); - - domHandler({ - name: 'click', - }); - - await waitForFlush(); - - expect(replay).not.toHaveLastSentReplay(); - expect(replay.isEnabled()).toBe(true); - expect(replay.isPaused()).toBe(false); - expect(replay.recordingMode).toBe('buffer'); - - // should still react to errors later on - captureException(new Error('testing')); - - await waitForBufferFlush(); - - expect(replay).toHaveLastSentReplay({ - recordingPayloadHeader: { segment_id: 0 }, - replayEventPayload: expect.objectContaining({ - replay_type: 'buffer', - }), - }); - - expect(replay.isEnabled()).toBe(true); - expect(replay.isPaused()).toBe(false); - expect(replay.recordingMode).toBe('session'); - expect(replay.session?.sampled).toBe('buffer'); - expect(replay.session?.shouldRefresh).toBe(false); - }); - - // Should behave the same as above test - it('stops replay if user has been idle for more than SESSION_IDLE_EXPIRE_DURATION and does not start a new session thereafter', async () => { - // Idle for 15 minutes - jest.advanceTimersByTime(SESSION_IDLE_EXPIRE_DURATION + 1); - - const TEST_EVENT = { - data: { name: 'lost event' }, - timestamp: BASE_TIMESTAMP, - type: 3, - }; - mockRecord._emitter(TEST_EVENT); - expect(replay).not.toHaveLastSentReplay(); - - jest.runAllTimers(); - await new Promise(process.nextTick); - - // We stop recording after SESSION_IDLE_EXPIRE_DURATION of inactivity in error mode - expect(replay).not.toHaveLastSentReplay(); - expect(replay.isEnabled()).toBe(true); - expect(replay.isPaused()).toBe(false); - expect(replay.recordingMode).toBe('buffer'); - - // should still react to errors later on - captureException(new Error('testing')); - - await new Promise(process.nextTick); - jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY); - await new Promise(process.nextTick); - - expect(replay).toHaveLastSentReplay({ - recordingPayloadHeader: { segment_id: 0 }, - replayEventPayload: expect.objectContaining({ - replay_type: 'buffer', - }), - }); - - expect(replay.isEnabled()).toBe(true); - expect(replay.isPaused()).toBe(false); - expect(replay.recordingMode).toBe('session'); - expect(replay.session?.sampled).toBe('buffer'); - expect(replay.session?.shouldRefresh).toBe(false); - }); - - it('has the correct timestamps with deferred root event and last replay update', async () => { - const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 3 }; - mockRecord._emitter(TEST_EVENT); - const optionsEvent = createOptionsEvent(replay); - - expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); - expect(replay).not.toHaveLastSentReplay(); - - jest.runAllTimers(); - await new Promise(process.nextTick); - - jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY); - - captureException(new Error('testing')); - - await new Promise(process.nextTick); - jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY); - await new Promise(process.nextTick); - - expect(replay).toHaveSentReplay({ - recordingData: JSON.stringify([ - { data: { isCheckout: true }, timestamp: BASE_TIMESTAMP, type: 2 }, - optionsEvent, - TEST_EVENT, - ]), - replayEventPayload: expect.objectContaining({ - replay_start_timestamp: BASE_TIMESTAMP / 1000, - // the exception happens roughly 10 seconds after BASE_TIMESTAMP - // (advance timers + waiting for flush after the checkout) and - // extra time is likely due to async of `addMemoryEntry()` - - timestamp: (BASE_TIMESTAMP + DEFAULT_FLUSH_MIN_DELAY + DEFAULT_FLUSH_MIN_DELAY + 40) / 1000, - error_ids: [expect.any(String)], - trace_ids: [], - urls: ['http://localhost/'], - replay_id: expect.any(String), - }), - recordingPayloadHeader: { segment_id: 0 }, - }); - }); - - it('has correct timestamps when error occurs much later than initial pageload/checkout', async () => { - const ELAPSED = BUFFER_CHECKOUT_TIME; - const TICK = 20; - const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 3 }; - mockRecord._emitter(TEST_EVENT); - - // add a mock performance event - replay.performanceEvents.push(PerformanceEntryResource()); - - jest.runAllTimers(); - await new Promise(process.nextTick); - - expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); - expect(replay).not.toHaveLastSentReplay(); - - jest.advanceTimersByTime(ELAPSED); - - // in production, this happens at a time interval - // session started time should be updated to this current timestamp - mockRecord.takeFullSnapshot(true); - const optionsEvent = createOptionsEvent(replay); - - jest.runAllTimers(); - await new Promise(process.nextTick); - - expect(replay).not.toHaveLastSentReplay(); - - captureException(new Error('testing')); - - await waitForBufferFlush(); - - // See comments in `handleRecordingEmit.ts`, we perform a setTimeout into a - // noop when it can be skipped altogether - expect(replay.session?.started).toBe(BASE_TIMESTAMP + ELAPSED + DEFAULT_FLUSH_MIN_DELAY + TICK + TICK); - - // Does not capture mouse click - expect(replay).toHaveSentReplay({ - recordingPayloadHeader: { segment_id: 0 }, - replayEventPayload: expect.objectContaining({ - // Make sure the old performance event is thrown out - replay_start_timestamp: (BASE_TIMESTAMP + ELAPSED + TICK) / 1000, - }), - recordingData: JSON.stringify([ - { - data: { isCheckout: true }, - timestamp: BASE_TIMESTAMP + ELAPSED + TICK, - type: 2, - }, - optionsEvent, - ]), - }); - }); - - it('stops replay when user goes idle', async () => { - jest.setSystemTime(BASE_TIMESTAMP); - - const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 3 }; - mockRecord._emitter(TEST_EVENT); - - expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); - expect(replay).not.toHaveLastSentReplay(); - - jest.runAllTimers(); - await new Promise(process.nextTick); - - captureException(new Error('testing')); - - await waitForBufferFlush(); - - expect(replay).toHaveLastSentReplay(); - - // Flush from calling `stopRecording` - await waitForFlush(); - - // Now wait after session expires - should stop recording - mockRecord.takeFullSnapshot.mockClear(); - (getCurrentHub().getClient()!.getTransport()!.send as unknown as jest.SpyInstance).mockClear(); - - expect(replay).not.toHaveLastSentReplay(); - - // Go idle - jest.advanceTimersByTime(SESSION_IDLE_EXPIRE_DURATION + 1); - await new Promise(process.nextTick); - - mockRecord._emitter(TEST_EVENT); - - expect(replay).not.toHaveLastSentReplay(); - - await waitForFlush(); - - expect(replay).not.toHaveLastSentReplay(); - expect(mockRecord.takeFullSnapshot).toHaveBeenCalledTimes(0); - expect(replay.isEnabled()).toBe(false); - }); - - it('stops replay when session exceeds max length after latest captured error', async () => { - const sessionId = replay.session?.id; - jest.setSystemTime(BASE_TIMESTAMP); - - const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 3 }; - mockRecord._emitter(TEST_EVENT); - - expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); - expect(replay).not.toHaveLastSentReplay(); - - jest.runAllTimers(); - await new Promise(process.nextTick); - - jest.advanceTimersByTime(2 * MAX_SESSION_LIFE); - - captureException(new Error('testing')); - - // Flush due to exception - await new Promise(process.nextTick); - await waitForFlush(); - - expect(replay.session?.id).toBe(sessionId); - expect(replay).toHaveLastSentReplay({ - recordingPayloadHeader: { segment_id: 0 }, - }); - - // This comes from `startRecording()` in `sendBufferedReplayOrFlush()` - await waitForFlush(); - expect(replay).toHaveLastSentReplay({ - recordingPayloadHeader: { segment_id: 1 }, - recordingData: JSON.stringify([ - { - data: { - isCheckout: true, - }, - timestamp: BASE_TIMESTAMP + 2 * MAX_SESSION_LIFE + DEFAULT_FLUSH_MIN_DELAY + 40, - type: 2, - }, - ]), - }); - - // Now wait after session expires - should stop recording - mockRecord.takeFullSnapshot.mockClear(); - (getCurrentHub().getClient()!.getTransport()!.send as unknown as jest.SpyInstance).mockClear(); - - jest.advanceTimersByTime(MAX_SESSION_LIFE); - await new Promise(process.nextTick); - - mockRecord._emitter(TEST_EVENT); - jest.runAllTimers(); - await new Promise(process.nextTick); - - expect(replay).not.toHaveLastSentReplay(); - expect(mockRecord.takeFullSnapshot).toHaveBeenCalledTimes(0); - expect(replay.isEnabled()).toBe(false); - - // Once the session is stopped after capturing a replay already - // (buffer-mode), another error will not trigger a new replay - captureException(new Error('testing')); - - await new Promise(process.nextTick); - jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY); - await new Promise(process.nextTick); - expect(replay).not.toHaveLastSentReplay(); - }); - - it('does not stop replay based on earliest event in buffer', async () => { - jest.setSystemTime(BASE_TIMESTAMP); - - const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP - 60000, type: 3 }; - mockRecord._emitter(TEST_EVENT); - - expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); - expect(replay).not.toHaveLastSentReplay(); - - jest.runAllTimers(); - await new Promise(process.nextTick); - - expect(replay).not.toHaveLastSentReplay(); - captureException(new Error('testing')); - - await waitForBufferFlush(); - - expect(replay).toHaveLastSentReplay(); - - // Flush from calling `stopRecording` - await waitForFlush(); - - // Now wait after session expires - should stop recording - mockRecord.takeFullSnapshot.mockClear(); - (getCurrentHub().getClient()!.getTransport()!.send as unknown as jest.SpyInstance).mockClear(); - - expect(replay).not.toHaveLastSentReplay(); - - const TICKS = 80; - - // We advance time so that we are on the border of expiring, taking into - // account that TEST_EVENT timestamp is 60000 ms before BASE_TIMESTAMP. The - // 3 DEFAULT_FLUSH_MIN_DELAY is to account for the `waitForFlush` that has - // happened, and for the next two that will happen. The first following - // `waitForFlush` does not expire session, but the following one will. - jest.advanceTimersByTime(SESSION_IDLE_EXPIRE_DURATION - 60000 - 3 * DEFAULT_FLUSH_MIN_DELAY - TICKS); - await new Promise(process.nextTick); - - mockRecord._emitter(TEST_EVENT); - expect(replay).not.toHaveLastSentReplay(); - await waitForFlush(); - - expect(replay).not.toHaveLastSentReplay(); - expect(mockRecord.takeFullSnapshot).toHaveBeenCalledTimes(0); - expect(replay.isEnabled()).toBe(true); - - mockRecord._emitter(TEST_EVENT); - expect(replay).not.toHaveLastSentReplay(); - await waitForFlush(); - - expect(replay).not.toHaveLastSentReplay(); - expect(mockRecord.takeFullSnapshot).toHaveBeenCalledTimes(0); - expect(replay.isEnabled()).toBe(true); - - // It's hard to test, but if we advance the below time less 1 ms, it should - // be enabled, but we can't trigger a session check via flush without - // incurring another DEFAULT_FLUSH_MIN_DELAY timeout. - jest.advanceTimersByTime(60000 - DEFAULT_FLUSH_MIN_DELAY); - mockRecord._emitter(TEST_EVENT); - expect(replay).not.toHaveLastSentReplay(); - await waitForFlush(); - - expect(replay).not.toHaveLastSentReplay(); - expect(mockRecord.takeFullSnapshot).toHaveBeenCalledTimes(0); - expect(replay.isEnabled()).toBe(false); - }); -}); - -/** - * This is testing a case that should only happen with error-only sessions. - * Previously we had assumed that loading a session from session storage meant - * that the session was not new. However, this is not the case with error-only - * sampling since we can load a saved session that did not have an error (and - * thus no replay was created). - */ -it('sends a replay after loading the session from storage', async () => { - // Pretend that a session is already saved before loading replay - WINDOW.sessionStorage.setItem( - REPLAY_SESSION_KEY, - `{"segmentId":0,"id":"fd09adfc4117477abc8de643e5a5798a","sampled":"buffer","started":${BASE_TIMESTAMP},"lastActivity":${BASE_TIMESTAMP}}`, - ); - const { mockRecord, replay, integration } = await resetSdkMock({ - replayOptions: { - stickySession: true, - _experiments: { - delayFlushOnCheckout: DEFAULT_FLUSH_MIN_DELAY, - }, - }, - sentryOptions: { - replaysOnErrorSampleRate: 1.0, - }, - autoStart: false, - }); - integration['_initialize'](); - const optionsEvent = createOptionsEvent(replay); - - jest.runAllTimers(); - - await new Promise(process.nextTick); - const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 3 }; - mockRecord._emitter(TEST_EVENT); - - expect(replay).not.toHaveLastSentReplay(); - - captureException(new Error('testing')); - - // 2 ticks to send replay from an error - await waitForBufferFlush(); - - // Buffered events before error - expect(replay).toHaveSentReplay({ - recordingPayloadHeader: { segment_id: 0 }, - recordingData: JSON.stringify([ - { data: { isCheckout: true }, timestamp: BASE_TIMESTAMP, type: 2 }, - optionsEvent, - TEST_EVENT, - ]), - }); - - // `startRecording()` after switching to session mode to continue recording - await waitForFlush(); - - // Latest checkout when we call `startRecording` again after uploading segment - // after an error occurs (e.g. when we switch to session replay recording) - expect(replay).toHaveLastSentReplay({ - recordingPayloadHeader: { segment_id: 1 }, - recordingData: JSON.stringify([ - { data: { isCheckout: true }, timestamp: BASE_TIMESTAMP + DEFAULT_FLUSH_MIN_DELAY + 40, type: 2 }, - ]), - }); -}); diff --git a/packages/replay/test/integration/errorSampleRate.test.ts b/packages/replay/test/integration/errorSampleRate.test.ts index ea1825dd8429..fe3049f9704f 100644 --- a/packages/replay/test/integration/errorSampleRate.test.ts +++ b/packages/replay/test/integration/errorSampleRate.test.ts @@ -26,6 +26,15 @@ async function advanceTimers(time: number) { await new Promise(process.nextTick); } +async function waitForBufferFlush() { + await new Promise(process.nextTick); + await new Promise(process.nextTick); +} + +async function waitForFlush() { + await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); +} + describe('Integration | errorSampleRate', () => { let replay: ReplayContainer; let mockRecord: RecordMock; @@ -66,11 +75,9 @@ describe('Integration | errorSampleRate', () => { captureException(new Error('testing')); - await new Promise(process.nextTick); - jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY); - await new Promise(process.nextTick); + await waitForBufferFlush(); - expect(replay).toHaveSentReplay({ + expect(replay).toHaveLastSentReplay({ recordingPayloadHeader: { segment_id: 0 }, replayEventPayload: expect.objectContaining({ replay_type: 'buffer', @@ -96,48 +103,35 @@ describe('Integration | errorSampleRate', () => { ]), }); + await waitForFlush(); + // This is from when we stop recording and start a session recording expect(replay).toHaveLastSentReplay({ recordingPayloadHeader: { segment_id: 1 }, replayEventPayload: expect.objectContaining({ replay_type: 'buffer', }), - recordingData: JSON.stringify([ - { data: { isCheckout: true }, timestamp: BASE_TIMESTAMP + DEFAULT_FLUSH_MIN_DELAY + 40, type: 2 }, - ]), + recordingData: JSON.stringify([{ data: { isCheckout: true }, timestamp: BASE_TIMESTAMP + 40, type: 2 }]), }); jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY); - // New checkout when we call `startRecording` again after uploading segment - // after an error occurs - expect(replay).toHaveLastSentReplay({ - recordingData: JSON.stringify([ - { - data: { isCheckout: true }, - timestamp: BASE_TIMESTAMP + DEFAULT_FLUSH_MIN_DELAY + 40, - type: 2, - }, - ]), - }); - // Check that click will get captured domHandler({ name: 'click', }); - jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY); - await new Promise(process.nextTick); + await waitForFlush(); expect(replay).toHaveLastSentReplay({ recordingData: JSON.stringify([ { type: 5, - timestamp: BASE_TIMESTAMP + 10000 + 60, + timestamp: BASE_TIMESTAMP + 10000 + 80, data: { tag: 'breadcrumb', payload: { - timestamp: (BASE_TIMESTAMP + 10000 + 60) / 1000, + timestamp: (BASE_TIMESTAMP + 10000 + 80) / 1000, type: 'default', category: 'ui.click', message: '', @@ -167,9 +161,7 @@ describe('Integration | errorSampleRate', () => { replay.sendBufferedReplayOrFlush({ continueRecording: false }); - await new Promise(process.nextTick); - jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY); - await new Promise(process.nextTick); + await waitForBufferFlush(); expect(replay).toHaveSentReplay({ recordingPayloadHeader: { segment_id: 0 }, @@ -202,8 +194,8 @@ describe('Integration | errorSampleRate', () => { domHandler({ name: 'click', }); - jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY); - await new Promise(process.nextTick); + + await waitForFlush(); // This is still the last replay sent since we passed `continueRecording: // false`. @@ -353,12 +345,12 @@ describe('Integration | errorSampleRate', () => { expect(replay).not.toHaveLastSentReplay(); // There should also not be another attempt at an upload 5 seconds after the last replay event - await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); + await waitForFlush(); expect(replay).not.toHaveLastSentReplay(); // Let's make sure it continues to work mockRecord._emitter(TEST_EVENT); - await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); + await waitForFlush(); jest.runAllTimers(); await new Promise(process.nextTick); expect(replay).not.toHaveLastSentReplay(); @@ -380,9 +372,16 @@ describe('Integration | errorSampleRate', () => { captureException(new Error('testing')); - await new Promise(process.nextTick); - jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY); - await new Promise(process.nextTick); + await waitForBufferFlush(); + + expect(replay).toHaveLastSentReplay({ + recordingPayloadHeader: { segment_id: 0 }, + replayEventPayload: expect.objectContaining({ + replay_type: 'buffer', + }), + }); + + await waitForFlush(); // segment_id is 1 because it sends twice on error expect(replay).toHaveLastSentReplay({ @@ -462,9 +461,7 @@ describe('Integration | errorSampleRate', () => { name: 'click', }); - await new Promise(process.nextTick); - jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY); - await new Promise(process.nextTick); + await waitForFlush(); expect(replay).not.toHaveLastSentReplay(); expect(replay.isEnabled()).toBe(true); @@ -474,23 +471,12 @@ describe('Integration | errorSampleRate', () => { // should still react to errors later on captureException(new Error('testing')); - await new Promise(process.nextTick); - jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY); - await new Promise(process.nextTick); + await waitForBufferFlush(); expect(replay.session?.id).toBe(oldSessionId); - // Flush of buffered events - expect(replay).toHaveSentReplay({ - recordingPayloadHeader: { segment_id: 0 }, - replayEventPayload: expect.objectContaining({ - replay_type: 'buffer', - }), - }); - - // Checkout from `startRecording` expect(replay).toHaveLastSentReplay({ - recordingPayloadHeader: { segment_id: 1 }, + recordingPayloadHeader: { segment_id: 0 }, replayEventPayload: expect.objectContaining({ replay_type: 'buffer', }), @@ -546,7 +532,7 @@ describe('Integration | errorSampleRate', () => { // `startRecording` full checkout expect(replay).toHaveLastSentReplay({ - recordingPayloadHeader: { segment_id: 1 }, + recordingPayloadHeader: { segment_id: 0 }, replayEventPayload: expect.objectContaining({ replay_type: 'buffer', }), @@ -602,6 +588,7 @@ describe('Integration | errorSampleRate', () => { it('has correct timestamps when error occurs much later than initial pageload/checkout', async () => { const ELAPSED = BUFFER_CHECKOUT_TIME; + const TICK = 20; const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 3 }; mockRecord._emitter(TEST_EVENT); @@ -624,25 +611,25 @@ describe('Integration | errorSampleRate', () => { jest.runAllTimers(); await new Promise(process.nextTick); + expect(replay).not.toHaveLastSentReplay(); + captureException(new Error('testing')); - await new Promise(process.nextTick); - jest.runAllTimers(); - await new Promise(process.nextTick); + await waitForBufferFlush(); - expect(replay.session?.started).toBe(BASE_TIMESTAMP + ELAPSED + 40); + expect(replay.session?.started).toBe(BASE_TIMESTAMP + ELAPSED + TICK + TICK); // Does not capture mouse click expect(replay).toHaveSentReplay({ recordingPayloadHeader: { segment_id: 0 }, replayEventPayload: expect.objectContaining({ // Make sure the old performance event is thrown out - replay_start_timestamp: (BASE_TIMESTAMP + ELAPSED + 20) / 1000, + replay_start_timestamp: (BASE_TIMESTAMP + ELAPSED + TICK) / 1000, }), recordingData: JSON.stringify([ { data: { isCheckout: true }, - timestamp: BASE_TIMESTAMP + ELAPSED + 20, + timestamp: BASE_TIMESTAMP + ELAPSED + TICK, type: 2, }, optionsEvent, @@ -664,12 +651,13 @@ describe('Integration | errorSampleRate', () => { captureException(new Error('testing')); - await new Promise(process.nextTick); - jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY); - await new Promise(process.nextTick); + await waitForBufferFlush(); expect(replay).toHaveLastSentReplay(); + // Flush from calling `stopRecording` + await waitForFlush(); + // Now wait after session expires - should stop recording mockRecord.takeFullSnapshot.mockClear(); (getCurrentHub().getClient()!.getTransport()!.send as unknown as jest.SpyInstance).mockClear(); @@ -684,8 +672,7 @@ describe('Integration | errorSampleRate', () => { expect(replay).not.toHaveLastSentReplay(); - jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY); - await new Promise(process.nextTick); + await waitForFlush(); expect(replay).not.toHaveLastSentReplay(); expect(mockRecord.takeFullSnapshot).toHaveBeenCalledTimes(0); @@ -711,12 +698,29 @@ describe('Integration | errorSampleRate', () => { // Flush due to exception await new Promise(process.nextTick); - jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY); - await new Promise(process.nextTick); + await waitForFlush(); + expect(replay.session?.id).toBe(sessionId); - expect(replay).toHaveLastSentReplay(); + expect(replay).toHaveLastSentReplay({ + recordingPayloadHeader: { segment_id: 0 }, + }); + + // This comes from `startRecording()` in `sendBufferedReplayOrFlush()` + await waitForFlush(); + expect(replay).toHaveLastSentReplay({ + recordingPayloadHeader: { segment_id: 1 }, + recordingData: JSON.stringify([ + { + data: { + isCheckout: true, + }, + timestamp: BASE_TIMESTAMP + 2 * MAX_SESSION_LIFE + DEFAULT_FLUSH_MIN_DELAY + 40, + type: 2, + }, + ]), + }); - // Now wait after session expires - should re-start into buffering mode + // Now wait after session expires - should stop recording mockRecord.takeFullSnapshot.mockClear(); (getCurrentHub().getClient()!.getTransport()!.send as unknown as jest.SpyInstance).mockClear(); @@ -732,7 +736,7 @@ describe('Integration | errorSampleRate', () => { expect(replay.isEnabled()).toBe(false); // Once the session is stopped after capturing a replay already - // (buffer-mode), another error should trigger a new replay + // (buffer-mode), another error will not trigger a new replay captureException(new Error('testing')); await new Promise(process.nextTick); @@ -740,6 +744,73 @@ describe('Integration | errorSampleRate', () => { await new Promise(process.nextTick); expect(replay).not.toHaveLastSentReplay(); }); + + it('does not stop replay based on earliest event in buffer', async () => { + jest.setSystemTime(BASE_TIMESTAMP); + + const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP - 60000, type: 3 }; + mockRecord._emitter(TEST_EVENT); + + expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); + expect(replay).not.toHaveLastSentReplay(); + + jest.runAllTimers(); + await new Promise(process.nextTick); + + expect(replay).not.toHaveLastSentReplay(); + captureException(new Error('testing')); + + await waitForBufferFlush(); + + expect(replay).toHaveLastSentReplay(); + + // Flush from calling `stopRecording` + await waitForFlush(); + + // Now wait after session expires - should stop recording + mockRecord.takeFullSnapshot.mockClear(); + (getCurrentHub().getClient()!.getTransport()!.send as unknown as jest.SpyInstance).mockClear(); + + expect(replay).not.toHaveLastSentReplay(); + + const TICKS = 80; + + // We advance time so that we are on the border of expiring, taking into + // account that TEST_EVENT timestamp is 60000 ms before BASE_TIMESTAMP. The + // 3 DEFAULT_FLUSH_MIN_DELAY is to account for the `waitForFlush` that has + // happened, and for the next two that will happen. The first following + // `waitForFlush` does not expire session, but the following one will. + jest.advanceTimersByTime(SESSION_IDLE_EXPIRE_DURATION - 60000 - 3 * DEFAULT_FLUSH_MIN_DELAY - TICKS); + await new Promise(process.nextTick); + + mockRecord._emitter(TEST_EVENT); + expect(replay).not.toHaveLastSentReplay(); + await waitForFlush(); + + expect(replay).not.toHaveLastSentReplay(); + expect(mockRecord.takeFullSnapshot).toHaveBeenCalledTimes(0); + expect(replay.isEnabled()).toBe(true); + + mockRecord._emitter(TEST_EVENT); + expect(replay).not.toHaveLastSentReplay(); + await waitForFlush(); + + expect(replay).not.toHaveLastSentReplay(); + expect(mockRecord.takeFullSnapshot).toHaveBeenCalledTimes(0); + expect(replay.isEnabled()).toBe(true); + + // It's hard to test, but if we advance the below time less 1 ms, it should + // be enabled, but we can't trigger a session check via flush without + // incurring another DEFAULT_FLUSH_MIN_DELAY timeout. + jest.advanceTimersByTime(60000 - DEFAULT_FLUSH_MIN_DELAY); + mockRecord._emitter(TEST_EVENT); + expect(replay).not.toHaveLastSentReplay(); + await waitForFlush(); + + expect(replay).not.toHaveLastSentReplay(); + expect(mockRecord.takeFullSnapshot).toHaveBeenCalledTimes(0); + expect(replay.isEnabled()).toBe(false); + }); }); /** @@ -749,7 +820,7 @@ describe('Integration | errorSampleRate', () => { * sampling since we can load a saved session that did not have an error (and * thus no replay was created). */ -it('sends a replay after loading the session multiple times', async () => { +it('sends a replay after loading the session from storage', async () => { // Pretend that a session is already saved before loading replay WINDOW.sessionStorage.setItem( REPLAY_SESSION_KEY, @@ -765,7 +836,6 @@ it('sends a replay after loading the session multiple times', async () => { autoStart: false, }); integration['_initialize'](); - const optionsEvent = createOptionsEvent(replay); jest.runAllTimers(); @@ -778,10 +848,10 @@ it('sends a replay after loading the session multiple times', async () => { captureException(new Error('testing')); - await new Promise(process.nextTick); - jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY); - await new Promise(process.nextTick); + // 2 ticks to send replay from an error + await waitForBufferFlush(); + // Buffered events before error expect(replay).toHaveSentReplay({ recordingPayloadHeader: { segment_id: 0 }, recordingData: JSON.stringify([ @@ -791,10 +861,13 @@ it('sends a replay after loading the session multiple times', async () => { ]), }); + // `startRecording()` after switching to session mode to continue recording + await waitForFlush(); + // Latest checkout when we call `startRecording` again after uploading segment // after an error occurs (e.g. when we switch to session replay recording) expect(replay).toHaveLastSentReplay({ recordingPayloadHeader: { segment_id: 1 }, - recordingData: JSON.stringify([{ data: { isCheckout: true }, timestamp: BASE_TIMESTAMP + 5040, type: 2 }]), + recordingData: JSON.stringify([{ data: { isCheckout: true }, timestamp: BASE_TIMESTAMP + 40, type: 2 }]), }); }); From 50ead2493dcc64f1a54fd54a6b3a6909e3005c43 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Mon, 19 Jun 2023 16:54:36 +0200 Subject: [PATCH 4/4] test(e2e): Add test for Next.js middleware (#8350) --- .../nextjs-app-dir/middleware.ts | 15 +++++ .../nextjs-app-dir/pages/api/edge-endpoint.ts | 18 +++++- .../pages/api/endpoint-behind-middleware.ts | 9 +++ .../pages/api/error-edge-endpoint.ts | 5 ++ .../nextjs-app-dir/tests/edge-route.test.ts | 56 +++++++++++++++++++ .../nextjs-app-dir/tests/middleware.test.ts | 53 ++++++++++++++++++ 6 files changed, 154 insertions(+), 2 deletions(-) create mode 100644 packages/e2e-tests/test-applications/nextjs-app-dir/middleware.ts create mode 100644 packages/e2e-tests/test-applications/nextjs-app-dir/pages/api/endpoint-behind-middleware.ts create mode 100644 packages/e2e-tests/test-applications/nextjs-app-dir/pages/api/error-edge-endpoint.ts create mode 100644 packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge-route.test.ts create mode 100644 packages/e2e-tests/test-applications/nextjs-app-dir/tests/middleware.test.ts diff --git a/packages/e2e-tests/test-applications/nextjs-app-dir/middleware.ts b/packages/e2e-tests/test-applications/nextjs-app-dir/middleware.ts new file mode 100644 index 000000000000..bb9db27b50d7 --- /dev/null +++ b/packages/e2e-tests/test-applications/nextjs-app-dir/middleware.ts @@ -0,0 +1,15 @@ +import { NextResponse } from 'next/server'; +import type { NextRequest } from 'next/server'; + +export function middleware(request: NextRequest) { + if (request.headers.has('x-should-throw')) { + throw new Error('Middleware Error'); + } + + return NextResponse.next(); +} + +// See "Matching Paths" below to learn more +export const config = { + matcher: ['/api/endpoint-behind-middleware'], +}; diff --git a/packages/e2e-tests/test-applications/nextjs-app-dir/pages/api/edge-endpoint.ts b/packages/e2e-tests/test-applications/nextjs-app-dir/pages/api/edge-endpoint.ts index d8af89f2e9d5..b2b2dfdf4fc3 100644 --- a/packages/e2e-tests/test-applications/nextjs-app-dir/pages/api/edge-endpoint.ts +++ b/packages/e2e-tests/test-applications/nextjs-app-dir/pages/api/edge-endpoint.ts @@ -1,3 +1,17 @@ -export const config = { runtime: 'edge' }; +export const config = { + runtime: 'edge', +}; -export default () => new Response('Hello world!'); +export default async function handler() { + return new Response( + JSON.stringify({ + name: 'Jim Halpert', + }), + { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }, + ); +} diff --git a/packages/e2e-tests/test-applications/nextjs-app-dir/pages/api/endpoint-behind-middleware.ts b/packages/e2e-tests/test-applications/nextjs-app-dir/pages/api/endpoint-behind-middleware.ts new file mode 100644 index 000000000000..2ca75a33ba7e --- /dev/null +++ b/packages/e2e-tests/test-applications/nextjs-app-dir/pages/api/endpoint-behind-middleware.ts @@ -0,0 +1,9 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; + +type Data = { + name: string; +}; + +export default function handler(req: NextApiRequest, res: NextApiResponse) { + res.status(200).json({ name: 'John Doe' }); +} diff --git a/packages/e2e-tests/test-applications/nextjs-app-dir/pages/api/error-edge-endpoint.ts b/packages/e2e-tests/test-applications/nextjs-app-dir/pages/api/error-edge-endpoint.ts new file mode 100644 index 000000000000..043112494c23 --- /dev/null +++ b/packages/e2e-tests/test-applications/nextjs-app-dir/pages/api/error-edge-endpoint.ts @@ -0,0 +1,5 @@ +export const config = { runtime: 'edge' }; + +export default () => { + throw new Error('Edge Route Error'); +}; diff --git a/packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge-route.test.ts b/packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge-route.test.ts new file mode 100644 index 000000000000..8f73764a919f --- /dev/null +++ b/packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge-route.test.ts @@ -0,0 +1,56 @@ +import { test, expect } from '@playwright/test'; +import { waitForTransaction, waitForError } from '../../../test-utils/event-proxy-server'; + +test('Should create a transaction for edge routes', async ({ request }) => { + test.skip(process.env.TEST_ENV === 'development', "Doesn't work in dev mode."); + + const edgerouteTransactionPromise = waitForTransaction('nextjs-13-app-dir', async transactionEvent => { + return ( + transactionEvent?.transaction === 'GET /api/edge-endpoint' && transactionEvent?.contexts?.trace?.status === 'ok' + ); + }); + + const response = await request.get('/api/edge-endpoint'); + expect(await response.json()).toStrictEqual({ name: 'Jim Halpert' }); + + const edgerouteTransaction = await edgerouteTransactionPromise; + + expect(edgerouteTransaction.contexts?.trace?.status).toBe('ok'); + expect(edgerouteTransaction.contexts?.trace?.op).toBe('http.server'); + expect(edgerouteTransaction.contexts?.runtime?.name).toBe('edge'); +}); + +test('Should create a transaction with error status for faulty edge routes', async ({ request }) => { + test.skip(process.env.TEST_ENV === 'development', "Doesn't work in dev mode."); + + const edgerouteTransactionPromise = waitForTransaction('nextjs-13-app-dir', async transactionEvent => { + return ( + transactionEvent?.transaction === 'GET /api/error-edge-endpoint' && + transactionEvent?.contexts?.trace?.status === 'internal_error' + ); + }); + + request.get('/api/error-edge-endpoint').catch(() => { + // Noop + }); + + const edgerouteTransaction = await edgerouteTransactionPromise; + + expect(edgerouteTransaction.contexts?.trace?.status).toBe('internal_error'); + expect(edgerouteTransaction.contexts?.trace?.op).toBe('http.server'); + expect(edgerouteTransaction.contexts?.runtime?.name).toBe('edge'); +}); + +test('Should record exceptions for faulty edge routes', async ({ request }) => { + test.skip(process.env.TEST_ENV === 'development', "Doesn't work in dev mode."); + + const errorEventPromise = waitForError('nextjs-13-app-dir', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Edge Route Error'; + }); + + request.get('/api/error-edge-endpoint').catch(() => { + // Noop + }); + + expect(await errorEventPromise).toBeDefined(); +}); diff --git a/packages/e2e-tests/test-applications/nextjs-app-dir/tests/middleware.test.ts b/packages/e2e-tests/test-applications/nextjs-app-dir/tests/middleware.test.ts new file mode 100644 index 000000000000..268a55f1f481 --- /dev/null +++ b/packages/e2e-tests/test-applications/nextjs-app-dir/tests/middleware.test.ts @@ -0,0 +1,53 @@ +import { test, expect } from '@playwright/test'; +import { waitForTransaction, waitForError } from '../../../test-utils/event-proxy-server'; + +test('Should create a transaction for middleware', async ({ request }) => { + test.skip(process.env.TEST_ENV === 'development', "Doesn't work in dev mode."); + + const middlewareTransactionPromise = waitForTransaction('nextjs-13-app-dir', async transactionEvent => { + return transactionEvent?.transaction === 'middleware' && transactionEvent?.contexts?.trace?.status === 'ok'; + }); + + const response = await request.get('/api/endpoint-behind-middleware'); + expect(await response.json()).toStrictEqual({ name: 'John Doe' }); + + const middlewareTransaction = await middlewareTransactionPromise; + + expect(middlewareTransaction.contexts?.trace?.status).toBe('ok'); + expect(middlewareTransaction.contexts?.trace?.op).toBe('middleware.nextjs'); + expect(middlewareTransaction.contexts?.runtime?.name).toBe('edge'); +}); + +test('Should create a transaction with error status for faulty middleware', async ({ request }) => { + test.skip(process.env.TEST_ENV === 'development', "Doesn't work in dev mode."); + + const middlewareTransactionPromise = waitForTransaction('nextjs-13-app-dir', async transactionEvent => { + return ( + transactionEvent?.transaction === 'middleware' && transactionEvent?.contexts?.trace?.status === 'internal_error' + ); + }); + + request.get('/api/endpoint-behind-middleware', { headers: { 'x-should-throw': '1' } }).catch(() => { + // Noop + }); + + const middlewareTransaction = await middlewareTransactionPromise; + + expect(middlewareTransaction.contexts?.trace?.status).toBe('internal_error'); + expect(middlewareTransaction.contexts?.trace?.op).toBe('middleware.nextjs'); + expect(middlewareTransaction.contexts?.runtime?.name).toBe('edge'); +}); + +test('Records exceptions happening in middleware', async ({ request }) => { + test.skip(process.env.TEST_ENV === 'development', "Doesn't work in dev mode."); + + const errorEventPromise = waitForError('nextjs-13-app-dir', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Middleware Error'; + }); + + request.get('/api/endpoint-behind-middleware', { headers: { 'x-should-throw': '1' } }).catch(() => { + // Noop + }); + + expect(await errorEventPromise).toBeDefined(); +});