diff --git a/.pnp.cjs b/.pnp.cjs index 067fd887a..08f108813 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -70,6 +70,10 @@ const RAW_RUNTIME_STATE = "name": "@stackflow/plugin-renderer-web",\ "reference": "workspace:extensions/plugin-renderer-web"\ },\ + {\ + "name": "@stackflow/plugin-sentry",\ + "reference": "workspace:extensions/plugin-sentry"\ + },\ {\ "name": "@stackflow/plugin-stack-depth-change",\ "reference": "workspace:extensions/plugin-stack-depth-change"\ @@ -106,6 +110,7 @@ const RAW_RUNTIME_STATE = ["@stackflow/plugin-preload", ["virtual:413bca98ff76262f6f1f73762ccc4b7edee04a5da42f3d6b9ed2cb2d6dbc397b2094da59b50f6e828091c88e7b5f86990feff596c43f0eb50a58fc42aae64a20#workspace:extensions/plugin-preload", "workspace:extensions/plugin-preload"]],\ ["@stackflow/plugin-renderer-basic", ["virtual:413bca98ff76262f6f1f73762ccc4b7edee04a5da42f3d6b9ed2cb2d6dbc397b2094da59b50f6e828091c88e7b5f86990feff596c43f0eb50a58fc42aae64a20#workspace:extensions/plugin-renderer-basic", "workspace:extensions/plugin-renderer-basic"]],\ ["@stackflow/plugin-renderer-web", ["workspace:extensions/plugin-renderer-web"]],\ + ["@stackflow/plugin-sentry", ["workspace:extensions/plugin-sentry"]],\ ["@stackflow/plugin-stack-depth-change", ["virtual:413bca98ff76262f6f1f73762ccc4b7edee04a5da42f3d6b9ed2cb2d6dbc397b2094da59b50f6e828091c88e7b5f86990feff596c43f0eb50a58fc42aae64a20#workspace:extensions/plugin-stack-depth-change", "workspace:extensions/plugin-stack-depth-change"]],\ ["@stackflow/react", ["virtual:413bca98ff76262f6f1f73762ccc4b7edee04a5da42f3d6b9ed2cb2d6dbc397b2094da59b50f6e828091c88e7b5f86990feff596c43f0eb50a58fc42aae64a20#workspace:integrations/react", "workspace:integrations/react"]],\ ["@stackflow/react-ui-core", ["virtual:669046a185e83900af978519e5adddf8e8f1f8fed824849248ba56cf8fcd4e4208872f27e14c3c844d3b769f42be1ba6e0aa90f12df9fa6c38a55aedee211f53#workspace:extensions/react-ui-core", "workspace:extensions/react-ui-core"]]\ @@ -2747,6 +2752,86 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["@sentry-internal/browser-utils", [\ + ["npm:8.40.0", {\ + "packageLocation": "./.yarn/cache/@sentry-internal-browser-utils-npm-8.40.0-0cb16ce8b5-b5f3bd4a8c.zip/node_modules/@sentry-internal/browser-utils/",\ + "packageDependencies": [\ + ["@sentry-internal/browser-utils", "npm:8.40.0"],\ + ["@sentry/core", "npm:8.40.0"],\ + ["@sentry/types", "npm:8.40.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@sentry-internal/feedback", [\ + ["npm:8.40.0", {\ + "packageLocation": "./.yarn/cache/@sentry-internal-feedback-npm-8.40.0-5547bafbc9-98e16aead7.zip/node_modules/@sentry-internal/feedback/",\ + "packageDependencies": [\ + ["@sentry-internal/feedback", "npm:8.40.0"],\ + ["@sentry/core", "npm:8.40.0"],\ + ["@sentry/types", "npm:8.40.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@sentry-internal/replay", [\ + ["npm:8.40.0", {\ + "packageLocation": "./.yarn/cache/@sentry-internal-replay-npm-8.40.0-bbc525e042-c2edb588f8.zip/node_modules/@sentry-internal/replay/",\ + "packageDependencies": [\ + ["@sentry-internal/replay", "npm:8.40.0"],\ + ["@sentry-internal/browser-utils", "npm:8.40.0"],\ + ["@sentry/core", "npm:8.40.0"],\ + ["@sentry/types", "npm:8.40.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@sentry-internal/replay-canvas", [\ + ["npm:8.40.0", {\ + "packageLocation": "./.yarn/cache/@sentry-internal-replay-canvas-npm-8.40.0-26450868fb-b219783f8b.zip/node_modules/@sentry-internal/replay-canvas/",\ + "packageDependencies": [\ + ["@sentry-internal/replay-canvas", "npm:8.40.0"],\ + ["@sentry-internal/replay", "npm:8.40.0"],\ + ["@sentry/core", "npm:8.40.0"],\ + ["@sentry/types", "npm:8.40.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@sentry/browser", [\ + ["npm:8.40.0", {\ + "packageLocation": "./.yarn/cache/@sentry-browser-npm-8.40.0-500840aa49-fe0bf8a598.zip/node_modules/@sentry/browser/",\ + "packageDependencies": [\ + ["@sentry/browser", "npm:8.40.0"],\ + ["@sentry-internal/browser-utils", "npm:8.40.0"],\ + ["@sentry-internal/feedback", "npm:8.40.0"],\ + ["@sentry-internal/replay", "npm:8.40.0"],\ + ["@sentry-internal/replay-canvas", "npm:8.40.0"],\ + ["@sentry/core", "npm:8.40.0"],\ + ["@sentry/types", "npm:8.40.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@sentry/core", [\ + ["npm:8.40.0", {\ + "packageLocation": "./.yarn/cache/@sentry-core-npm-8.40.0-389ff0edb3-450e2d3514.zip/node_modules/@sentry/core/",\ + "packageDependencies": [\ + ["@sentry/core", "npm:8.40.0"],\ + ["@sentry/types", "npm:8.40.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@sentry/types", [\ + ["npm:8.40.0", {\ + "packageLocation": "./.yarn/cache/@sentry-types-npm-8.40.0-f6513bfb87-6fba146cbd.zip/node_modules/@sentry/types/",\ + "packageDependencies": [\ + ["@sentry/types", "npm:8.40.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["@sinclair/typebox", [\ ["npm:0.27.8", {\ "packageLocation": "./.yarn/cache/@sinclair-typebox-npm-0.27.8-23e206d653-297f95ff77.zip/node_modules/@sinclair/typebox/",\ @@ -3333,6 +3418,22 @@ const RAW_RUNTIME_STATE = "linkType": "SOFT"\ }]\ ]],\ + ["@stackflow/plugin-sentry", [\ + ["workspace:extensions/plugin-sentry", {\ + "packageLocation": "./extensions/plugin-sentry/",\ + "packageDependencies": [\ + ["@stackflow/plugin-sentry", "workspace:extensions/plugin-sentry"],\ + ["@sentry/browser", "npm:8.40.0"],\ + ["@sentry/core", "npm:8.40.0"],\ + ["@sentry/types", "npm:8.40.0"],\ + ["@stackflow/core", "workspace:core"],\ + ["@stackflow/esbuild-config", "workspace:packages/esbuild-config"],\ + ["esbuild", "npm:0.23.0"],\ + ["typescript", "patch:typescript@npm%3A5.5.3#optional!builtin::version=5.5.3&hash=379a07"]\ + ],\ + "linkType": "SOFT"\ + }]\ + ]],\ ["@stackflow/plugin-stack-depth-change", [\ ["virtual:413bca98ff76262f6f1f73762ccc4b7edee04a5da42f3d6b9ed2cb2d6dbc397b2094da59b50f6e828091c88e7b5f86990feff596c43f0eb50a58fc42aae64a20#workspace:extensions/plugin-stack-depth-change", {\ "packageLocation": "./.yarn/__virtual__/@stackflow-plugin-stack-depth-change-virtual-05c63f7f2d/1/extensions/plugin-stack-depth-change/",\ diff --git a/extensions/plugin-sentry/README.md b/extensions/plugin-sentry/README.md new file mode 100644 index 000000000..7e210dffd --- /dev/null +++ b/extensions/plugin-sentry/README.md @@ -0,0 +1,25 @@ +# plugin-sentry + +Add Sentry plugin for analysis tracing activity events + +## Initialize + + +```typescript +import { stackflow } from "@stackflow/react"; +import { sentryPlugin } from "@stackflow/plugin-sentry"; + +const { Stack, useFlow } = stackflow({ + activities: { + // ... + }, + plugins: [ + sentryPlugin({ + dsn: "https://xxx.ingest.us.sentry.io/xxx", // Sentry project dsn key + // ... + // Additional Options for initiate Sentry + // https://docs.sentry.io/platforms/javascript/configuration/options/ + }), + ], +}); +``` \ No newline at end of file diff --git a/extensions/plugin-sentry/esbuild.config.js b/extensions/plugin-sentry/esbuild.config.js new file mode 100644 index 000000000..b84dfb4db --- /dev/null +++ b/extensions/plugin-sentry/esbuild.config.js @@ -0,0 +1,29 @@ +const { context } = require("esbuild"); +const config = require("@stackflow/esbuild-config"); +const pkg = require("./package.json"); + +const watch = process.argv.includes("--watch"); +const external = Object.keys({ + ...pkg.dependencies, + ...pkg.peerDependencies, +}); + +Promise.all([ + context({ + ...config({}), + format: "cjs", + external, + }).then((ctx) => + watch ? ctx.watch() : ctx.rebuild().then(() => ctx.dispose()), + ), + context({ + ...config({}), + format: "esm", + outExtension: { + ".js": ".mjs", + }, + external, + }).then((ctx) => + watch ? ctx.watch() : ctx.rebuild().then(() => ctx.dispose()), + ), +]).catch(() => process.exit(1)); diff --git a/extensions/plugin-sentry/package.json b/extensions/plugin-sentry/package.json new file mode 100644 index 000000000..0fd82ed65 --- /dev/null +++ b/extensions/plugin-sentry/package.json @@ -0,0 +1,43 @@ +{ + "name": "@stackflow/plugin-sentry", + "version": "0,0,0", + "repository": { + "type": "git", + "url": "https://github.com/daangn/stackflow.git", + "directory": "extensions/plugin-sentry" + }, + "license": "MIT", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "require": "./dist/index.js", + "import": "./dist/index.mjs" + } + }, + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "files": [ + "dist", + "src", + "README.md" + ], + "scripts": { + "build": "yarn build:js && yarn build:dts", + "build:dts": "tsc --emitDeclarationOnly", + "build:js": "node ./esbuild.config.js", + "clean": "rimraf dist", + "dev": "yarn build:js --watch && yarn build:dts --watch" + }, + "devDependencies": { + "@sentry/types": "^8.37.1", + "@stackflow/core": "^1.1.0", + "@stackflow/esbuild-config": "^1.0.3", + "esbuild": "^0.23.0", + "typescript": "^5.5.3" + }, + "dependencies": { + "@sentry/browser": "^8.37.1", + "@sentry/core": "^8.37.1" + } +} diff --git a/extensions/plugin-sentry/src/index.ts b/extensions/plugin-sentry/src/index.ts new file mode 100644 index 000000000..8170a5607 --- /dev/null +++ b/extensions/plugin-sentry/src/index.ts @@ -0,0 +1 @@ +export * from "./sentryPlugin"; diff --git a/extensions/plugin-sentry/src/integration.ts b/extensions/plugin-sentry/src/integration.ts new file mode 100644 index 000000000..e6eebb788 --- /dev/null +++ b/extensions/plugin-sentry/src/integration.ts @@ -0,0 +1,43 @@ +import { + WINDOW, + browserTracingIntegration as originalBrowserTracingIntegration, + startBrowserTracingPageLoadSpan, +} from "@sentry/browser"; +import { + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, +} from "@sentry/core"; + +import type { Integration } from "@sentry/types"; + +export function stackflowBrowserTracingIntegration( + options: Parameters[0] = {}, +): Integration { + const browserTracingIntegrationInstance = originalBrowserTracingIntegration({ + ...options, + instrumentNavigation: false, + instrumentPageLoad: false, + }); + const { instrumentPageLoad = true } = options; + + return { + ...browserTracingIntegrationInstance, + afterAllSetup(client) { + browserTracingIntegrationInstance.afterAllSetup(client); + + const initialWindowLocation = WINDOW.location; + + if (instrumentPageLoad && initialWindowLocation) { + startBrowserTracingPageLoadSpan(client, { + name: initialWindowLocation.pathname, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: "pageload", + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: "auto.pageload.stackflow", + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: "url", + }, + }); + } + }, + }; +} diff --git a/extensions/plugin-sentry/src/sentryPlugin.ts b/extensions/plugin-sentry/src/sentryPlugin.ts new file mode 100644 index 000000000..8639a2c9e --- /dev/null +++ b/extensions/plugin-sentry/src/sentryPlugin.ts @@ -0,0 +1,68 @@ +import * as Sentry from "@sentry/browser"; +import type { BrowserOptions } from "@sentry/browser"; +import { + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, +} from "@sentry/core"; +import type { Integration } from "@sentry/types"; +import type { + DomainEvent, + Effect, + Stack, + StackflowActions, + StackflowPlugin, +} from "@stackflow/core"; + +import { stackflowBrowserTracingIntegration } from "./integration"; + +export function sentryPlugin(options: BrowserOptions): StackflowPlugin { + return () => ({ + key: "plugin-sentry", + onInit() { + Sentry.init({ + ...options, + integrations: [ + stackflowBrowserTracingIntegration(), + ...(options.integrations as Integration[]), + ], + }); + }, + onPushed({ effect }) { + const client = Sentry.getClient(); + if (!client) return; + Sentry.startBrowserTracingNavigationSpan(client, { + name: `push ${effect.activity.name}`, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: "navigation", + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: "auto.navigation.stackflow", + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: "route", + }, + }); + }, + onPopped({ effect }) { + const client = Sentry.getClient(); + if (!client) return; + Sentry.startBrowserTracingNavigationSpan(client, { + name: `pop ${effect.activity.name}`, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: "navigation", + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: "auto.navigation.stackflow", + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: "route", + }, + }); + }, + onReplaced({ effect }) { + const client = Sentry.getClient(); + if (!client) return; + Sentry.startBrowserTracingNavigationSpan(client, { + name: `replace ${effect.activity.name}`, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: "navigation", + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: "auto.navigation.stackflow", + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: "route", + }, + }); + }, + }); +} diff --git a/extensions/plugin-sentry/tsconfig.json b/extensions/plugin-sentry/tsconfig.json new file mode 100644 index 000000000..b1d123121 --- /dev/null +++ b/extensions/plugin-sentry/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "baseUrl": "./src", + "outDir": "./dist" + }, + "exclude": ["./dist"] +} diff --git a/yarn.lock b/yarn.lock index 185f4f694..9592e4dc2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1970,6 +1970,78 @@ __metadata: languageName: node linkType: hard +"@sentry-internal/browser-utils@npm:8.40.0": + version: 8.40.0 + resolution: "@sentry-internal/browser-utils@npm:8.40.0" + dependencies: + "@sentry/core": "npm:8.40.0" + "@sentry/types": "npm:8.40.0" + checksum: 10/b5f3bd4a8c043801a5567b75b585591f5f52343ffd865cb02b88d8b3b5e7af0fe59faa9945403ebf03af6b45bd7a6604643c670af51dbe2f4bf2040b22399241 + languageName: node + linkType: hard + +"@sentry-internal/feedback@npm:8.40.0": + version: 8.40.0 + resolution: "@sentry-internal/feedback@npm:8.40.0" + dependencies: + "@sentry/core": "npm:8.40.0" + "@sentry/types": "npm:8.40.0" + checksum: 10/98e16aead7a5b635c83d826182f6f092debe0d4139a341929ba489d5fff92c82049ddc90de3a7252e04396912ac49a674e42dcd377136e971a65386950eb02e4 + languageName: node + linkType: hard + +"@sentry-internal/replay-canvas@npm:8.40.0": + version: 8.40.0 + resolution: "@sentry-internal/replay-canvas@npm:8.40.0" + dependencies: + "@sentry-internal/replay": "npm:8.40.0" + "@sentry/core": "npm:8.40.0" + "@sentry/types": "npm:8.40.0" + checksum: 10/b219783f8b2768ba9fbeaa13452567eaa6d10f5180a311390032d9d6b19f645098ee15c1daac5b17177d5329db26c6825f87edf0cd67d40b223d0bea4e762375 + languageName: node + linkType: hard + +"@sentry-internal/replay@npm:8.40.0": + version: 8.40.0 + resolution: "@sentry-internal/replay@npm:8.40.0" + dependencies: + "@sentry-internal/browser-utils": "npm:8.40.0" + "@sentry/core": "npm:8.40.0" + "@sentry/types": "npm:8.40.0" + checksum: 10/c2edb588f83ebfa99d2ee2702a9d1ca919cbb1a83539bfc279432f422e094d77741e4c07f5909a82d46ce619b4ed48fa04250546bd4f537868bd4519c87fb228 + languageName: node + linkType: hard + +"@sentry/browser@npm:^8.37.1": + version: 8.40.0 + resolution: "@sentry/browser@npm:8.40.0" + dependencies: + "@sentry-internal/browser-utils": "npm:8.40.0" + "@sentry-internal/feedback": "npm:8.40.0" + "@sentry-internal/replay": "npm:8.40.0" + "@sentry-internal/replay-canvas": "npm:8.40.0" + "@sentry/core": "npm:8.40.0" + "@sentry/types": "npm:8.40.0" + checksum: 10/fe0bf8a598dd366bc24a19bfe0ddf4c2b6a2998eab93f51a961a9136b3bfbda4a26610302e41d11520ba09efb27c101e1f3587af011fe09585a11f8605f75a2c + languageName: node + linkType: hard + +"@sentry/core@npm:8.40.0, @sentry/core@npm:^8.37.1": + version: 8.40.0 + resolution: "@sentry/core@npm:8.40.0" + dependencies: + "@sentry/types": "npm:8.40.0" + checksum: 10/450e2d35143a7918e1a5b4978c218df0f59ef10bd1b28fc92d1cbb491a0bebf28b2a1e8609361bd2070c9054c5e58d535de53cf3e3a46ecc75cf2c0e8a488aeb + languageName: node + linkType: hard + +"@sentry/types@npm:8.40.0, @sentry/types@npm:^8.37.1": + version: 8.40.0 + resolution: "@sentry/types@npm:8.40.0" + checksum: 10/6fba146cbd9bc89a16d0fcd8cb01e441e9720f8a72bb390038bbf25b1c2feeb7b18de437b755a0e70bca113d66357771055b695305a8923527fba89e096ed931 + languageName: node + linkType: hard + "@sinclair/typebox@npm:^0.27.8": version: 0.27.8 resolution: "@sinclair/typebox@npm:0.27.8" @@ -2342,6 +2414,20 @@ __metadata: languageName: unknown linkType: soft +"@stackflow/plugin-sentry@workspace:extensions/plugin-sentry": + version: 0.0.0-use.local + resolution: "@stackflow/plugin-sentry@workspace:extensions/plugin-sentry" + dependencies: + "@sentry/browser": "npm:^8.37.1" + "@sentry/core": "npm:^8.37.1" + "@sentry/types": "npm:^8.37.1" + "@stackflow/core": "npm:^1.1.0" + "@stackflow/esbuild-config": "npm:^1.0.3" + esbuild: "npm:^0.23.0" + typescript: "npm:^5.5.3" + languageName: unknown + linkType: soft + "@stackflow/plugin-stack-depth-change@npm:^1.1.5, @stackflow/plugin-stack-depth-change@workspace:extensions/plugin-stack-depth-change": version: 0.0.0-use.local resolution: "@stackflow/plugin-stack-depth-change@workspace:extensions/plugin-stack-depth-change"