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