-
Notifications
You must be signed in to change notification settings - Fork 15
Expand file tree
/
Copy pathclient.ts
More file actions
127 lines (111 loc) · 3.77 KB
/
client.ts
File metadata and controls
127 lines (111 loc) · 3.77 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
import type { PostHog } from "posthog-node"
import type { TelemetryConfig } from "./types"
const POSTHOG_API_KEY = process.env.POSTHOG_API_KEY || ""
const POSTHOG_HOST = "https://eu.i.posthog.com"
/** Flush after this many events are queued */
const FLUSH_AT = 10
/** Flush after this many milliseconds (30 seconds) */
const FLUSH_INTERVAL_MS = 30000
/** Python packages to track versions for */
export const TRACKED_PACKAGES = [
"fastapi",
"fastapi-cli",
"fastapi-cloud-cli",
"typer",
"starlette",
"pydantic",
] as const
export class TelemetryClient {
private posthog: PostHog | null = null
private userId: string | null = null
private config: TelemetryConfig | null = null
private initialized = false
private packageVersions: Record<string, string | undefined> = {}
private sessionId: string | null = null
private sessionStartTime: number | null = null
/* c8 ignore start -- requires PostHog API key */
async init(config: TelemetryConfig): Promise<void> {
if (this.initialized || !config.isEnabled() || !POSTHOG_API_KEY) return
this.config = config
this.userId = config.userId
this.sessionId = crypto.randomUUID()
this.sessionStartTime = Date.now()
let PostHogClass: typeof PostHog
try {
;({ PostHog: PostHogClass } = await import("posthog-node"))
} catch {
// posthog-node is unavailable (e.g. browser/web context)
return
}
this.posthog = new PostHogClass(POSTHOG_API_KEY, {
host: POSTHOG_HOST,
disableGeoip: true,
flushAt: FLUSH_AT,
flushInterval: FLUSH_INTERVAL_MS,
})
// Identify user with static properties available at init time.
// Python/FastAPI versions are added later via setVersions() and included in events,
// since they require async detection and can change during the session.
this.posthog.identify({
distinctId: this.userId,
properties: {
client: config.clientInfo.client,
app_name: config.clientInfo.app_name,
app_host: config.clientInfo.app_host,
is_remote: config.clientInfo.is_remote,
remote_name: config.clientInfo.remote_name,
extension_version: config.extensionVersion,
},
})
this.initialized = true
}
/* c8 ignore stop */
/**
* Set package versions to include in all events.
* Call this after detecting versions from the Python extension.
*/
setVersions(versions: Record<string, string | undefined>): void {
this.packageVersions = versions
}
getSessionDuration(): number | null {
if (!this.sessionStartTime) return null
return Date.now() - this.sessionStartTime
}
async shutdown(): Promise<void> {
/* c8 ignore next 4 -- requires PostHog instance */
if (this.posthog) {
await this.posthog.shutdown()
this.posthog = null
}
this.initialized = false
this.userId = null
this.config = null
this.packageVersions = {}
this.sessionId = null
this.sessionStartTime = null
}
/* c8 ignore start -- requires PostHog API key */
capture(event: string, properties?: Record<string, unknown>): void {
if (!this.posthog || !this.userId || !this.config?.isEnabled()) return
try {
this.posthog.capture({
distinctId: this.userId,
event,
properties: {
...properties,
client: this.config.clientInfo.client,
platform: this.config.clientInfo.platform,
arch: this.config.clientInfo.arch,
extension_version: this.config.extensionVersion,
...this.packageVersions,
$session_id: this.sessionId,
},
})
} catch (_error) {
// TODO: Log to Logfire when available
// Telemetry should never break the extension, so we silently catch errors
}
}
/* c8 ignore stop */
}
export const client = new TelemetryClient()