-
Notifications
You must be signed in to change notification settings - Fork 5
feat: add PostHog event tracking for SDK installs (ENG-2277) #298
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
bf061e6
ac3a3ca
1a7d7e3
9b67ddb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,214 @@ | ||||||||||||||||||||||||||||||||||||||||||||||
| import { fs, os, path } from "../common/node.js"; | ||||||||||||||||||||||||||||||||||||||||||||||
| import { settings } from "./settings.js"; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // PostHog API key injected at build time via build:replace-imports | ||||||||||||||||||||||||||||||||||||||||||||||
| const BUILD_POSTHOG_KEY = "__BUILD_POSTHOG_KEY__"; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // PostHog API endpoint | ||||||||||||||||||||||||||||||||||||||||||||||
| const POSTHOG_HOST = "https://us.i.posthog.com"; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // Telemetry state file path: ~/.blaxel/telemetry.json | ||||||||||||||||||||||||||||||||||||||||||||||
| type TelemetryState = { | ||||||||||||||||||||||||||||||||||||||||||||||
| distinct_id: string; | ||||||||||||||||||||||||||||||||||||||||||||||
| cli?: string; | ||||||||||||||||||||||||||||||||||||||||||||||
| sdks?: Record<string, string>; | ||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| let telemetryState: TelemetryState | null = null; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||
| * Get the PostHog API key (injected at build time). | ||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||
| function getPosthogKey(): string { | ||||||||||||||||||||||||||||||||||||||||||||||
| const key = BUILD_POSTHOG_KEY; | ||||||||||||||||||||||||||||||||||||||||||||||
| // If the placeholder was not replaced, treat as empty | ||||||||||||||||||||||||||||||||||||||||||||||
| if (!key || key.startsWith("__BUILD_")) { | ||||||||||||||||||||||||||||||||||||||||||||||
| return ""; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| return key; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||
| * Get the telemetry file path. | ||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||
| function getTelemetryPath(): string | null { | ||||||||||||||||||||||||||||||||||||||||||||||
| if (os === null || path === null) { | ||||||||||||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||
| return path.join(os.homedir(), ".blaxel", "telemetry.json"); | ||||||||||||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||
| * Load telemetry state from disk. | ||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||
| function loadTelemetryState(): TelemetryState { | ||||||||||||||||||||||||||||||||||||||||||||||
| if (telemetryState !== null) { | ||||||||||||||||||||||||||||||||||||||||||||||
| return telemetryState; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| telemetryState = { distinct_id: "", sdks: {} }; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| if (fs === null) { | ||||||||||||||||||||||||||||||||||||||||||||||
| return telemetryState; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const telemetryPath = getTelemetryPath(); | ||||||||||||||||||||||||||||||||||||||||||||||
| if (!telemetryPath) { | ||||||||||||||||||||||||||||||||||||||||||||||
| return telemetryState; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||
| const data = fs.readFileSync(telemetryPath, "utf8"); | ||||||||||||||||||||||||||||||||||||||||||||||
| const parsed = JSON.parse(data) as TelemetryState; | ||||||||||||||||||||||||||||||||||||||||||||||
| telemetryState = { | ||||||||||||||||||||||||||||||||||||||||||||||
| ...parsed, | ||||||||||||||||||||||||||||||||||||||||||||||
| distinct_id: parsed.distinct_id || "", | ||||||||||||||||||||||||||||||||||||||||||||||
| sdks: parsed.sdks || {}, | ||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||
|
devin-ai-integration[bot] marked this conversation as resolved.
|
||||||||||||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||||||||||||
| // File doesn't exist or is invalid - use defaults | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| return telemetryState; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||
| * Save telemetry state to disk. | ||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||
| function saveTelemetryState(state: TelemetryState): void { | ||||||||||||||||||||||||||||||||||||||||||||||
| if (fs === null || path === null || os === null) { | ||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const telemetryPath = getTelemetryPath(); | ||||||||||||||||||||||||||||||||||||||||||||||
| if (!telemetryPath) { | ||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||
| const dir = path.dirname(telemetryPath); | ||||||||||||||||||||||||||||||||||||||||||||||
| if (!fs.existsSync(dir)) { | ||||||||||||||||||||||||||||||||||||||||||||||
| fs.mkdirSync(dir, { recursive: true }); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| fs.writeFileSync(telemetryPath, JSON.stringify(state, null, 2), { | ||||||||||||||||||||||||||||||||||||||||||||||
| mode: 0o600, | ||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||||||||||||
| // Silently fail | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||
| * Generate a UUID v4 without external dependencies. | ||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||
| function generateUUID(): string { | ||||||||||||||||||||||||||||||||||||||||||||||
| const bytes = new Uint8Array(16); | ||||||||||||||||||||||||||||||||||||||||||||||
| crypto.getRandomValues(bytes); | ||||||||||||||||||||||||||||||||||||||||||||||
| bytes[6] = (bytes[6] & 0x0f) | 0x40; // version 4 | ||||||||||||||||||||||||||||||||||||||||||||||
| bytes[8] = (bytes[8] & 0x3f) | 0x80; // variant 10 | ||||||||||||||||||||||||||||||||||||||||||||||
| const hex = Array.from(bytes) | ||||||||||||||||||||||||||||||||||||||||||||||
| .map((b) => b.toString(16).padStart(2, "0")) | ||||||||||||||||||||||||||||||||||||||||||||||
| .join(""); | ||||||||||||||||||||||||||||||||||||||||||||||
| return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||
| * Get or create a persistent distinct ID for PostHog events. | ||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||
| function getDistinctID(): string { | ||||||||||||||||||||||||||||||||||||||||||||||
| const state = loadTelemetryState(); | ||||||||||||||||||||||||||||||||||||||||||||||
| if (state.distinct_id) { | ||||||||||||||||||||||||||||||||||||||||||||||
| return state.distinct_id; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| state.distinct_id = generateUUID(); | ||||||||||||||||||||||||||||||||||||||||||||||
| saveTelemetryState(state); | ||||||||||||||||||||||||||||||||||||||||||||||
| return state.distinct_id; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||
| * Send an event to PostHog via HTTP POST. Fire-and-forget. | ||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||
| function capturePosthogEvent( | ||||||||||||||||||||||||||||||||||||||||||||||
| event: string, | ||||||||||||||||||||||||||||||||||||||||||||||
| properties: Record<string, string>, | ||||||||||||||||||||||||||||||||||||||||||||||
| ): void { | ||||||||||||||||||||||||||||||||||||||||||||||
| const apiKey = getPosthogKey(); | ||||||||||||||||||||||||||||||||||||||||||||||
| if (!apiKey) { | ||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const distinctId = getDistinctID(); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const payload = { | ||||||||||||||||||||||||||||||||||||||||||||||
| api_key: apiKey, | ||||||||||||||||||||||||||||||||||||||||||||||
| event, | ||||||||||||||||||||||||||||||||||||||||||||||
| distinct_id: distinctId, | ||||||||||||||||||||||||||||||||||||||||||||||
| timestamp: new Date().toISOString(), | ||||||||||||||||||||||||||||||||||||||||||||||
| properties, | ||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||
| fetch(`${POSTHOG_HOST}/capture/`, { | ||||||||||||||||||||||||||||||||||||||||||||||
| method: "POST", | ||||||||||||||||||||||||||||||||||||||||||||||
| headers: { "Content-Type": "application/json" }, | ||||||||||||||||||||||||||||||||||||||||||||||
| body: JSON.stringify(payload), | ||||||||||||||||||||||||||||||||||||||||||||||
| signal: AbortSignal.timeout(5000), | ||||||||||||||||||||||||||||||||||||||||||||||
| }).catch(() => { | ||||||||||||||||||||||||||||||||||||||||||||||
| // Silently fail - telemetry should never break the SDK | ||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||||||||||||
| // Silently fail | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||
| * Track SDK installation. Fires "Installed SDK" once per new version. | ||||||||||||||||||||||||||||||||||||||||||||||
| * Called during SDK autoload. | ||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||
| export function trackSDKInstalled(): void { | ||||||||||||||||||||||||||||||||||||||||||||||
| const apiKey = getPosthogKey(); | ||||||||||||||||||||||||||||||||||||||||||||||
| if (!apiKey) { | ||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // Only track if tracking is enabled | ||||||||||||||||||||||||||||||||||||||||||||||
| if (!settings.tracking) { | ||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
mendral-app[bot] marked this conversation as resolved.
mendral-app[bot] marked this conversation as resolved.
mendral-app[bot] marked this conversation as resolved.
Comment on lines
+179
to
+181
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. bug (P0): Suggested change
Suggested change
Prompt for AI agents
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is by design — |
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const sdkVersion = settings.version; | ||||||||||||||||||||||||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||||||||||||||||||||||||
| !sdkVersion || | ||||||||||||||||||||||||||||||||||||||||||||||
| sdkVersion === "unknown" || | ||||||||||||||||||||||||||||||||||||||||||||||
| sdkVersion === "__BUILD_VERSION__" | ||||||||||||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // Only run in Node.js (not in browsers) | ||||||||||||||||||||||||||||||||||||||||||||||
| if (typeof process === "undefined" || !process.versions?.node) { | ||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const state = loadTelemetryState(); | ||||||||||||||||||||||||||||||||||||||||||||||
| const sdkName = "typescript"; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| if (state.sdks && state.sdks[sdkName] === sdkVersion) { | ||||||||||||||||||||||||||||||||||||||||||||||
| return; // Already tracked this version | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| capturePosthogEvent("Installed SDK", { | ||||||||||||||||||||||||||||||||||||||||||||||
| version: sdkVersion, | ||||||||||||||||||||||||||||||||||||||||||||||
| language: sdkName, | ||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| if (!state.sdks) { | ||||||||||||||||||||||||||||||||||||||||||||||
| state.sdks = {}; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| state.sdks[sdkName] = sdkVersion; | ||||||||||||||||||||||||||||||||||||||||||||||
| saveTelemetryState(state); | ||||||||||||||||||||||||||||||||||||||||||||||
|
mendral-app[bot] marked this conversation as resolved.
mendral-app[bot] marked this conversation as resolved.
mendral-app[bot] marked this conversation as resolved.
Comment on lines
+204
to
+213
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. bug (P2): The version is saved to disk before the fire-and-forget fetch resolves. A silent network failure permanently suppresses retries for that version. Suggested change
Suggested change
Prompt for AI agents
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is intentional optimistic dedup — the same pattern used by all three repos (toolkit, sdk-typescript, sdk-python). The tradeoff is accepted: worst case on network failure is one missed event per version, which is far preferable to the alternative of spamming PostHog on every SDK load if we defer the state write. Note that the bot's own suggested fix preserves the exact same save-before-fire ordering. |
||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.