diff --git a/packages/devtools/package.json b/packages/devtools/package.json
index 496137e0d..39463f0cd 100644
--- a/packages/devtools/package.json
+++ b/packages/devtools/package.json
@@ -1,6 +1,6 @@
{
"name": "@openuidev/devtools",
- "version": "0.1.0",
+ "version": "0.1.1",
"description": "Development-only UI widget for OpenUI apps: surfaces errors captured by @openuidev/observability",
"license": "MIT",
"type": "module",
diff --git a/packages/devtools/src/inspect/QuotaErrorRow.tsx b/packages/devtools/src/inspect/QuotaErrorRow.tsx
index 15735119f..72601ae62 100644
--- a/packages/devtools/src/inspect/QuotaErrorRow.tsx
+++ b/packages/devtools/src/inspect/QuotaErrorRow.tsx
@@ -1,6 +1,7 @@
import { type ObservabilityEvent } from "@openuidev/observability";
import { CreditCard, KeyRound } from "lucide-react";
import { useState, type CSSProperties } from "react";
+import { withDevtoolsAttribution } from "../lib/links";
import { FONT, useStyles, type ThemeTokens } from "../theme";
import { LevelIcon } from "./LevelIcon";
@@ -61,7 +62,14 @@ export function QuotaErrorRow({ info }: { info: QuotaErrorInfo }) {
onMouseEnter={() => setHoveredCta("purchase")}
onMouseLeave={() => setHoveredCta(null)}
onClick={() =>
- window.open("https://console.thesys.dev/billing", "_blank", "noopener,noreferrer")
+ window.open(
+ withDevtoolsAttribution(
+ "https://console.thesys.dev/billing",
+ "quota_error_purchase_credits",
+ ),
+ "_blank",
+ "noopener,noreferrer",
+ )
}
>
@@ -79,7 +87,11 @@ export function QuotaErrorRow({ info }: { info: QuotaErrorInfo }) {
onMouseEnter={() => setHoveredCta("byok")}
onMouseLeave={() => setHoveredCta(null)}
onClick={() =>
- window.open("https://console.thesys.dev/byok", "_blank", "noopener,noreferrer")
+ window.open(
+ withDevtoolsAttribution("https://console.thesys.dev/byok", "quota_error_byok"),
+ "_blank",
+ "noopener,noreferrer",
+ )
}
>
diff --git a/packages/devtools/src/lib/index.ts b/packages/devtools/src/lib/index.ts
index c2b86a207..89beadca0 100644
--- a/packages/devtools/src/lib/index.ts
+++ b/packages/devtools/src/lib/index.ts
@@ -8,5 +8,6 @@ export {
type LibraryLike,
type RegisteredLibrary,
} from "./libraryRegistry";
+export { withDevtoolsAttribution } from "./links";
export { useDevtoolsSingleton } from "./singleton";
export { useDevtoolsConfig, type DevtoolsConfig } from "./useDevtoolsConfig";
diff --git a/packages/devtools/src/lib/links.ts b/packages/devtools/src/lib/links.ts
new file mode 100644
index 000000000..9f361e40e
--- /dev/null
+++ b/packages/devtools/src/lib/links.ts
@@ -0,0 +1,24 @@
+const UTM_PARAMS = {
+ utm_source: "openui",
+ utm_medium: "referral",
+ utm_campaign: "openui_devtools",
+} as const;
+
+/** Tags an outbound devtools link so referrals are attributable, keeping existing params. */
+export function withDevtoolsAttribution(href: string, content: string) {
+ let url: URL;
+
+ try {
+ url = new URL(href);
+ } catch {
+ return href;
+ }
+
+ for (const [key, value] of Object.entries(UTM_PARAMS)) {
+ if (!url.searchParams.has(key)) url.searchParams.set(key, value);
+ }
+
+ if (!url.searchParams.has("utm_content")) url.searchParams.set("utm_content", content);
+
+ return url.toString();
+}
diff --git a/packages/devtools/src/ui/ReliabilityBanner.tsx b/packages/devtools/src/ui/ReliabilityBanner.tsx
index 91e0ab0cb..4663b7efc 100644
--- a/packages/devtools/src/ui/ReliabilityBanner.tsx
+++ b/packages/devtools/src/ui/ReliabilityBanner.tsx
@@ -1,4 +1,5 @@
import { CSSProperties, useState } from "react";
+import { withDevtoolsAttribution } from "../lib/links";
import { ColorMode, FONT, theme, ThemeTokens } from "../theme";
export default function ReliabilityBanner({ themeMode }: { themeMode: ColorMode }) {
@@ -10,7 +11,10 @@ export default function ReliabilityBanner({ themeMode }: { themeMode: ColorMode