Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-plugin-sentry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@stackflow/plugin-sentry": minor
---

fix(plugin-sentry): decouple Sentry init from plugin, fix dependency structure, remove internal API usage
85 changes: 85 additions & 0 deletions .pnp.cjs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
37 changes: 37 additions & 0 deletions extensions/plugin-sentry/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# plugin-sentry

Stackflow plugin for Sentry browser tracing. Automatically creates navigation spans and breadcrumbs for activity transitions (`push`, `pop`, `replace`) and step transitions (`stepPush`, `stepPop`, `stepReplace`).

## Setup

1. Initialize Sentry with the `stackflowBrowserTracingIntegration`:

```typescript
import * as Sentry from "@sentry/browser";
import { stackflowBrowserTracingIntegration } from "@stackflow/plugin-sentry";

Sentry.init({
dsn: "https://xxx.ingest.us.sentry.io/xxx",
integrations: [
stackflowBrowserTracingIntegration(),
// ... other integrations
],
});
```

2. Add `sentryPlugin()` to your stackflow configuration:

```typescript
import { stackflow } from "@stackflow/react";
import { sentryPlugin } from "@stackflow/plugin-sentry";

const { Stack, useFlow } = stackflow({
activities: {
// ...
},
plugins: [
sentryPlugin(),
// ... other plugins
],
});
```
29 changes: 29 additions & 0 deletions extensions/plugin-sentry/esbuild.config.js
Original file line number Diff line number Diff line change
@@ -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));
45 changes: 45 additions & 0 deletions extensions/plugin-sentry/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"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"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

dev script won’t run both watchers.

yarn build:js --watch is long-running, so yarn build:dts --watch never starts with &&.

🔧 Proposed fix
-    "dev": "yarn build:js --watch && yarn build:dts --watch"
+    "dev": "concurrently \"yarn build:js --watch\" \"yarn build:dts --watch\""
   "devDependencies": {
+    "concurrently": "^9.0.0",
     "@sentry/browser": "^10.46.0",
     "@sentry/core": "^10.46.0",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@extensions/plugin-sentry/package.json` at line 30, The dev script in
package.json uses "yarn build:js --watch && yarn build:dts --watch" so the
second watcher never starts; change the dev script to run both watchers
concurrently (e.g., use the "concurrently" tool to run "yarn build:js --watch"
and "yarn build:dts --watch" in parallel) and add "concurrently" to
devDependencies if it's not already installed so both watchers (build:js and
build:dts) run at the same time.

},
"peerDependencies": {
"@sentry/browser": ">=8.0.0",
"@sentry/core": ">=8.0.0",
"@stackflow/core": "^1.1.0-canary.0"
},
"devDependencies": {
"@sentry/browser": "^10.46.0",
"@sentry/core": "^10.46.0",
"@stackflow/core": "^1.1.0",
"@stackflow/esbuild-config": "^1.0.3",
"esbuild": "^0.23.0",
"typescript": "^5.5.3"
}
}
2 changes: 2 additions & 0 deletions extensions/plugin-sentry/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { sentryPlugin } from "./sentryPlugin";
export { stackflowBrowserTracingIntegration } from "./integration";
43 changes: 43 additions & 0 deletions extensions/plugin-sentry/src/integration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import {
browserTracingIntegration as originalBrowserTracingIntegration,
startBrowserTracingPageLoadSpan,
} from "@sentry/browser";
import {
SEMANTIC_ATTRIBUTE_SENTRY_OP,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
} from "@sentry/core";

export function stackflowBrowserTracingIntegration(
options: Parameters<typeof originalBrowserTracingIntegration>[0] = {},
) {
const browserTracingIntegrationInstance = originalBrowserTracingIntegration({
...options,
instrumentNavigation: false,
instrumentPageLoad: false,
});
const { instrumentPageLoad = true } = options;

return {
...browserTracingIntegrationInstance,
afterAllSetup(
client: Parameters<typeof browserTracingIntegrationInstance.afterAllSetup>[0],
) {
browserTracingIntegrationInstance.afterAllSetup(client);

const initialWindowLocation =
typeof window !== "undefined" ? window.location : undefined;

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",
},
});
}
},
};
}
103 changes: 103 additions & 0 deletions extensions/plugin-sentry/src/sentryPlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import * as Sentry from "@sentry/browser";
import {
SEMANTIC_ATTRIBUTE_SENTRY_OP,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
} from "@sentry/core";
import type { StackflowPlugin } from "@stackflow/core";

type NavigationAction =
| "push"
| "pop"
| "replace"
| "stepPush"
| "stepPop"
| "stepReplace";

function startNavigationSpan(
action: NavigationAction,
activityName: string,
params: Record<string, string | undefined>,
): void {
const client = Sentry.getClient();
if (!client) return;

Sentry.startBrowserTracingNavigationSpan(client, {
name: `${action} ${activityName}`,
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: "navigation",
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: "auto.navigation.stackflow",
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: "route",
"stackflow.action": action,
"stackflow.activity": activityName,
...prefixKeys("stackflow.params", params),
},
});
}

function addNavigationBreadcrumb(
action: NavigationAction,
activityName: string,
params: Record<string, string | undefined>,
): void {
Sentry.addBreadcrumb({
category: "navigation",
message: `${action} ${activityName}`,
level: "info",
data: params,
});
}

function prefixKeys(
prefix: string,
params: Record<string, string | undefined>,
): Record<string, string> {
const result: Record<string, string> = {};
for (const [key, value] of Object.entries(params)) {
if (value !== undefined) {
result[`${prefix}.${key}`] = value;
}
}
return result;
}

export function sentryPlugin(): StackflowPlugin {
return () => ({
key: "plugin-sentry",
onPushed({ effect }) {
const { name, params } = effect.activity;
startNavigationSpan("push", name, params);
addNavigationBreadcrumb("push", name, params);
},
onPopped({ effect }) {
const { name, params } = effect.activity;
startNavigationSpan("pop", name, params);
addNavigationBreadcrumb("pop", name, params);
},
onReplaced({ effect }) {
const { name, params } = effect.activity;
startNavigationSpan("replace", name, params);
addNavigationBreadcrumb("replace", name, params);
},
onStepPushed({ effect }) {
const { name, params } = effect.activity;
const stepParams = effect.step.params;
startNavigationSpan("stepPush", name, { ...params, ...stepParams });
addNavigationBreadcrumb("stepPush", name, { ...params, ...stepParams });
},
onStepPopped({ effect }) {
const { name, params } = effect.activity;
startNavigationSpan("stepPop", name, params);
addNavigationBreadcrumb("stepPop", name, params);
},
onStepReplaced({ effect }) {
const { name, params } = effect.activity;
const stepParams = effect.step.params;
startNavigationSpan("stepReplace", name, { ...params, ...stepParams });
addNavigationBreadcrumb("stepReplace", name, {
...params,
...stepParams,
});
},
});
}
8 changes: 8 additions & 0 deletions extensions/plugin-sentry/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"baseUrl": "./src",
"outDir": "./dist"
},
"exclude": ["./dist"]
}
Loading
Loading