Skip to content
Merged
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
---

feat(plugin-sentry): add Sentry browser tracing plugin for Stackflow navigation events
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"
},
"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