forked from liuup/claude-code-analysis
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathuser.ts
More file actions
194 lines (174 loc) · 5.58 KB
/
user.ts
File metadata and controls
194 lines (174 loc) · 5.58 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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
import { execa } from 'execa'
import memoize from 'lodash-es/memoize.js'
import { getSessionId } from '../bootstrap/state.js'
import {
getOauthAccountInfo,
getRateLimitTier,
getSubscriptionType,
} from './auth.js'
import { getGlobalConfig, getOrCreateUserID } from './config.js'
import { getCwd } from './cwd.js'
import { type env, getHostPlatformForAnalytics } from './env.js'
import { isEnvTruthy } from './envUtils.js'
// Cache for email fetched asynchronously at startup
let cachedEmail: string | undefined | null = null // null means not fetched yet
let emailFetchPromise: Promise<string | undefined> | null = null
/**
* GitHub Actions metadata when running in CI
*/
export type GitHubActionsMetadata = {
actor?: string
actorId?: string
repository?: string
repositoryId?: string
repositoryOwner?: string
repositoryOwnerId?: string
}
/**
* Core user data used as base for all analytics providers.
* This is also the format used by GrowthBook.
*/
export type CoreUserData = {
deviceId: string
sessionId: string
email?: string
appVersion: string
platform: typeof env.platform
organizationUuid?: string
accountUuid?: string
userType?: string
subscriptionType?: string
rateLimitTier?: string
firstTokenTime?: number
githubActionsMetadata?: GitHubActionsMetadata
}
/**
* Initialize user data asynchronously. Should be called early in startup.
* This pre-fetches the email so getUser() can remain synchronous.
*/
export async function initUser(): Promise<void> {
if (cachedEmail === null && !emailFetchPromise) {
emailFetchPromise = getEmailAsync()
cachedEmail = await emailFetchPromise
emailFetchPromise = null
// Clear memoization cache so next call picks up the email
getCoreUserData.cache.clear?.()
}
}
/**
* Reset all user data caches. Call on auth changes (login/logout/account switch)
* so the next getCoreUserData() call picks up fresh credentials and email.
*/
export function resetUserCache(): void {
cachedEmail = null
emailFetchPromise = null
getCoreUserData.cache.clear?.()
getGitEmail.cache.clear?.()
}
/**
* Get core user data.
* This is the base representation that gets transformed for different analytics providers.
*/
export const getCoreUserData = memoize(
(includeAnalyticsMetadata?: boolean): CoreUserData => {
const deviceId = getOrCreateUserID()
const config = getGlobalConfig()
let subscriptionType: string | undefined
let rateLimitTier: string | undefined
let firstTokenTime: number | undefined
if (includeAnalyticsMetadata) {
subscriptionType = getSubscriptionType() ?? undefined
rateLimitTier = getRateLimitTier() ?? undefined
if (subscriptionType && config.claudeCodeFirstTokenDate) {
const configFirstTokenTime = new Date(
config.claudeCodeFirstTokenDate,
).getTime()
if (!isNaN(configFirstTokenTime)) {
firstTokenTime = configFirstTokenTime
}
}
}
// Only include OAuth account data when actively using OAuth authentication
const oauthAccount = getOauthAccountInfo()
const organizationUuid = oauthAccount?.organizationUuid
const accountUuid = oauthAccount?.accountUuid
return {
deviceId,
sessionId: getSessionId(),
email: getEmail(),
appVersion: MACRO.VERSION,
platform: getHostPlatformForAnalytics(),
organizationUuid,
accountUuid,
userType: process.env.USER_TYPE,
subscriptionType,
rateLimitTier,
firstTokenTime,
...(isEnvTruthy(process.env.GITHUB_ACTIONS) && {
githubActionsMetadata: {
actor: process.env.GITHUB_ACTOR,
actorId: process.env.GITHUB_ACTOR_ID,
repository: process.env.GITHUB_REPOSITORY,
repositoryId: process.env.GITHUB_REPOSITORY_ID,
repositoryOwner: process.env.GITHUB_REPOSITORY_OWNER,
repositoryOwnerId: process.env.GITHUB_REPOSITORY_OWNER_ID,
},
}),
}
},
)
/**
* Get user data for GrowthBook (same as core data with analytics metadata).
*/
export function getUserForGrowthBook(): CoreUserData {
return getCoreUserData(true)
}
function getEmail(): string | undefined {
// Return cached email if available (from async initialization)
if (cachedEmail !== null) {
return cachedEmail
}
// Only include OAuth email when actively using OAuth authentication
const oauthAccount = getOauthAccountInfo()
if (oauthAccount?.emailAddress) {
return oauthAccount.emailAddress
}
// Ant-only fallbacks below (no execSync)
if (process.env.USER_TYPE !== 'ant') {
return undefined
}
if (process.env.COO_CREATOR) {
return `${process.env.COO_CREATOR}@anthropic.com`
}
// If initUser() wasn't called, we return undefined instead of blocking
return undefined
}
async function getEmailAsync(): Promise<string | undefined> {
// Only include OAuth email when actively using OAuth authentication
const oauthAccount = getOauthAccountInfo()
if (oauthAccount?.emailAddress) {
return oauthAccount.emailAddress
}
// Ant-only fallbacks below
if (process.env.USER_TYPE !== 'ant') {
return undefined
}
if (process.env.COO_CREATOR) {
return `${process.env.COO_CREATOR}@anthropic.com`
}
return getGitEmail()
}
/**
* Get the user's git email from `git config user.email`.
* Memoized so the subprocess only spawns once per process.
*/
export const getGitEmail = memoize(async (): Promise<string | undefined> => {
const result = await execa('git config --get user.email', {
shell: true,
reject: false,
cwd: getCwd(),
})
return result.exitCode === 0 && result.stdout
? result.stdout.trim()
: undefined
})