11import withPostHog from "@/analytics" ;
22import { globalPrismaClient } from "@/prisma-client" ;
33import { runAsynchronouslyAndWaitUntil } from "@/utils/vercel" ;
4- import { urlSchema , yupBoolean , yupMixed , yupObject , yupString } from "@stackframe/stack-shared/dist/schema-fields" ;
4+ import { urlSchema , yupBoolean , yupMixed , yupNumber , yupObject , yupString } from "@stackframe/stack-shared/dist/schema-fields" ;
55import { getEnvVariable , getNodeEnvironment } from "@stackframe/stack-shared/dist/utils/env" ;
66import { StackAssertionError , throwErr } from "@stackframe/stack-shared/dist/utils/errors" ;
77import { HTTP_METHODS } from "@stackframe/stack-shared/dist/utils/http" ;
@@ -13,6 +13,42 @@ import { getClickhouseAdminClient, isClickhouseConfigured } from "./clickhouse";
1313import { getEndUserInfo } from "./end-users" ;
1414import { DEFAULT_BRANCH_ID } from "./tenancies" ;
1515
16+ export const endUserIpInfoSchema = yupObject ( {
17+ ip : yupString ( ) . defined ( ) ,
18+ isTrusted : yupBoolean ( ) . defined ( ) ,
19+ countryCode : yupString ( ) . optional ( ) ,
20+ regionCode : yupString ( ) . optional ( ) ,
21+ cityName : yupString ( ) . optional ( ) ,
22+ latitude : yupNumber ( ) . optional ( ) ,
23+ longitude : yupNumber ( ) . optional ( ) ,
24+ tzIdentifier : yupString ( ) . optional ( ) ,
25+ } ) ;
26+
27+ export type EndUserIpInfo = yup . InferType < typeof endUserIpInfoSchema > ;
28+
29+ /**
30+ * Extracts the end user IP info from the current request.
31+ * Must be called before any async operations as it uses dynamic APIs.
32+ */
33+ export async function getEndUserIpInfoForEvent ( ) : Promise < EndUserIpInfo | null > {
34+ const endUserInfo = await getEndUserInfo ( ) ;
35+ if ( ! endUserInfo ) {
36+ return null ;
37+ }
38+
39+ const info = endUserInfo . maybeSpoofed ? endUserInfo . spoofedInfo : endUserInfo . exactInfo ;
40+ return {
41+ ip : info . ip ,
42+ isTrusted : ! endUserInfo . maybeSpoofed ,
43+ countryCode : info . countryCode ,
44+ regionCode : info . regionCode ,
45+ cityName : info . cityName ,
46+ latitude : info . latitude ,
47+ longitude : info . longitude ,
48+ tzIdentifier : info . tzIdentifier ,
49+ } ;
50+ }
51+
1652type EventType = {
1753 id : string ,
1854 dataSchema : yup . Schema < any > ,
@@ -65,6 +101,20 @@ const SessionActivityEventType = {
65101 inherits : [ UserActivityEventType ] ,
66102} as const satisfies SystemEventTypeBase ;
67103
104+ const TokenRefreshEventType = {
105+ id : "$token-refresh" ,
106+ dataSchema : yupObject ( {
107+ projectId : yupString ( ) . defined ( ) ,
108+ branchId : yupString ( ) . defined ( ) ,
109+ organizationId : yupString ( ) . nullable ( ) . test ( "must-be-null" , "Organization ID has not been implemented yet and must be null" , ( value ) => value === null ) . defined ( ) ,
110+ userId : yupString ( ) . uuid ( ) . defined ( ) ,
111+ refreshTokenId : yupString ( ) . defined ( ) ,
112+ isAnonymous : yupBoolean ( ) . defined ( ) ,
113+ ipInfo : endUserIpInfoSchema . nullable ( ) . defined ( ) ,
114+ } ) ,
115+ inherits : [ ] ,
116+ } as const satisfies SystemEventTypeBase ;
117+
68118
69119const ApiRequestEventType = {
70120 id : "$api-request" ,
@@ -84,6 +134,7 @@ export const SystemEventTypes = stripEventTypeSuffixFromKeys({
84134 ProjectActivityEventType,
85135 UserActivityEventType,
86136 SessionActivityEventType,
137+ TokenRefreshEventType,
87138 ApiRequestEventType,
88139 LegacyApiEventType,
89140} as const ) ;
@@ -165,9 +216,6 @@ export async function logEvent<T extends EventType[]>(
165216 const projectId = typeof dataRecord === "object" && dataRecord && typeof dataRecord . projectId === "string" ? dataRecord . projectId : "" ;
166217 const branchId = typeof dataRecord === "object" && dataRecord && typeof dataRecord . branchId === "string" ? dataRecord . branchId : DEFAULT_BRANCH_ID ;
167218 const userId = typeof dataRecord === "object" && dataRecord && typeof dataRecord . userId === "string" ? dataRecord . userId : "" ;
168- const teamId = typeof dataRecord === "object" && dataRecord && typeof dataRecord . teamId === "string" ? dataRecord . teamId : "" ;
169- const sessionId = typeof dataRecord === "object" && dataRecord && typeof dataRecord . sessionId === "string" ? dataRecord . sessionId : "" ;
170- const isAnonymous = typeof dataRecord === "object" && dataRecord && typeof dataRecord . isAnonymous === "boolean" ? dataRecord . isAnonymous : false ;
171219
172220
173221 // rest is no more dynamic APIs so we can run it asynchronously
@@ -195,21 +243,20 @@ export async function logEvent<T extends EventType[]>(
195243 } ,
196244 } ) ;
197245
198- if ( isClickhouseConfigured ( ) ) {
246+ // Only log TokenRefresh events to ClickHouse
247+ if ( isClickhouseConfigured ( ) && eventTypesArray . some ( e => e . id === '$token-refresh' ) ) {
199248 const clickhouseClient = getClickhouseAdminClient ( ) ;
200249 await clickhouseClient . insert ( {
201250 table : "analytics_internal.events" ,
202- values : eventTypesArray . map ( eventType => ( {
203- event_type : eventType . id ,
251+ values : [ {
252+ event_type : '$token-refresh' ,
204253 event_at : timeRange . end ,
205254 data : clickhouseEventData ,
206255 project_id : projectId ,
207256 branch_id : branchId ,
208- user_id : userId ,
209- team_id : teamId ,
210- is_anonymous : isAnonymous ,
211- session_id : sessionId ,
212- } ) ) ,
257+ user_id : userId || null ,
258+ team_id : null , // Token refresh events don't have team context
259+ } ] ,
213260 format : "JSONEachRow" ,
214261 clickhouse_settings : {
215262 date_time_input_format : "best_effort" ,
0 commit comments