1
1
import type { TelemetryEventRaw } from '@clerk/types' ;
2
+ import { promises as fs } from 'fs' ;
3
+ import { join } from 'path' ;
2
4
3
5
import { createClerkClientWithOptions } from './createClerkClient' ;
4
6
5
7
const EVENT_KEYLESS_ENV_DRIFT_DETECTED = 'KEYLESS_ENV_DRIFT_DETECTED' ;
6
8
const EVENT_SAMPLING_RATE = 1 ; // 100% sampling rate
9
+ const TELEMETRY_FLAG_FILE = '.clerk/.tmp/telemetry.json' ;
7
10
8
11
type EventKeylessEnvDriftPayload = {
9
12
publicKeyMatch : boolean ;
@@ -15,14 +18,61 @@ type EventKeylessEnvDriftPayload = {
15
18
} ;
16
19
17
20
/**
18
- * Detects environment variable drift for keyless Next.js applications and fires telemetry events .
21
+ * Gets the absolute path to the telemetry flag file .
19
22
*
20
- * This function compares the publishableKey and secretKey values from `.clerk/.tmp/keyless.json`
21
- * with the `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` and `CLERK_SECRET_KEY` environment variables .
23
+ * This file is used to track whether telemetry events have already been fired
24
+ * to prevent duplicate event reporting during the application lifecycle .
22
25
*
23
- * If there's a mismatch, it fires a `KEYLESS_ENV_DRIFT_DETECTED` event.
24
- * For local testing purposes, it also fires a `KEYLESS_ENV_DRIFT_NOT_DETECTED` event when
25
- * keys exist and match the environment variables.
26
+ * @returns The absolute path to the telemetry flag file in the project's .clerk/.tmp directory
27
+ */
28
+ function getTelemetryFlagFilePath ( ) : string {
29
+ return join ( process . cwd ( ) , TELEMETRY_FLAG_FILE ) ;
30
+ }
31
+
32
+ /**
33
+ * Attempts to create a telemetry flag file to mark that a telemetry event has been fired.
34
+ *
35
+ * This function uses the 'wx' flag to create the file atomically - it will only succeed
36
+ * if the file doesn't already exist. This ensures that telemetry events are only fired
37
+ * once per application lifecycle, preventing duplicate event reporting.
38
+ *
39
+ * @returns Promise<boolean> - Returns true if the flag file was successfully created (meaning
40
+ * the event should be fired), false if the file already exists (meaning the event was
41
+ * already fired) or if there was an error creating the file
42
+ */
43
+ async function tryMarkTelemetryEventAsFired ( ) : Promise < boolean > {
44
+ try {
45
+ const flagData = {
46
+ firedAt : new Date ( ) . toISOString ( ) ,
47
+ event : EVENT_KEYLESS_ENV_DRIFT_DETECTED ,
48
+ } ;
49
+ await fs . writeFile ( getTelemetryFlagFilePath ( ) , JSON . stringify ( flagData , null , 2 ) , { flag : 'wx' } ) ;
50
+ return true ;
51
+ } catch ( error : unknown ) {
52
+ if ( ( error as { code ?: string } ) ?. code === 'EEXIST' ) {
53
+ return false ;
54
+ }
55
+ console . warn ( 'Failed to create telemetry flag file:' , error ) ;
56
+ return false ;
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Detects and reports environment drift between keyless configuration and environment variables.
62
+ *
63
+ * This function compares the Clerk keys stored in the keyless configuration file (.clerk/clerk.json)
64
+ * with the keys set in environment variables (NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY and CLERK_SECRET_KEY).
65
+ * If there's a mismatch (drift), it reports this as a telemetry event to help diagnose configuration issues.
66
+ *
67
+ * The function handles several scenarios:
68
+ * - Normal keyless mode: env vars missing but keyless file has keys (no drift)
69
+ * - Drift detected: env vars and keyless file have different keys
70
+ * - Mixed configuration: some keys match, others don't
71
+ *
72
+ * Telemetry events are only fired once per application lifecycle using a flag file mechanism
73
+ * to prevent duplicate reporting.
74
+ *
75
+ * @returns Promise<void> - Function completes silently, errors are logged but don't throw
26
76
*/
27
77
export async function detectKeylessEnvDrift ( ) : Promise < void > {
28
78
// Only run on server side
@@ -55,10 +105,11 @@ export async function detectKeylessEnvDrift(): Promise<void> {
55
105
}
56
106
57
107
// Compare publishable keys
58
- const publicKeyMatch = Boolean ( envPublishableKey === keylessFile ?. publishableKey ) ;
59
-
108
+ const publicKeyMatch = Boolean (
109
+ envPublishableKey && keylessFile ?. publishableKey && envPublishableKey === keylessFile . publishableKey ,
110
+ ) ;
60
111
// Compare secret keys
61
- const secretKeyMatch = Boolean ( envSecretKey === keylessFile ? .secretKey ) ;
112
+ const secretKeyMatch = Boolean ( envSecretKey && keylessFile ?. secretKey && envSecretKey === keylessFile . secretKey ) ;
62
113
63
114
// Check if there's a drift (mismatch between env vars and keyless file)
64
115
const hasDrift = ! publicKeyMatch || ! secretKeyMatch ;
@@ -69,7 +120,7 @@ export async function detectKeylessEnvDrift(): Promise<void> {
69
120
envVarsMissing,
70
121
keylessFileHasKeys,
71
122
keylessPublishableKey : keylessFile . publishableKey ,
72
- envPublishableKey : envPublishableKey as string ,
123
+ envPublishableKey : envPublishableKey ?? '' ,
73
124
} ;
74
125
75
126
// Create a clerk client to access telemetry
@@ -79,14 +130,18 @@ export async function detectKeylessEnvDrift(): Promise<void> {
79
130
} ) ;
80
131
81
132
if ( hasDrift ) {
82
- // Fire drift detected event
83
- const driftDetectedEvent : TelemetryEventRaw < EventKeylessEnvDriftPayload > = {
84
- event : EVENT_KEYLESS_ENV_DRIFT_DETECTED ,
85
- eventSamplingRate : EVENT_SAMPLING_RATE ,
86
- payload,
87
- } ;
88
-
89
- clerkClient . telemetry ?. record ( driftDetectedEvent ) ;
133
+ const shouldFireEvent = await tryMarkTelemetryEventAsFired ( ) ;
134
+
135
+ if ( shouldFireEvent ) {
136
+ // Fire drift detected event only if we successfully created the flag
137
+ const driftDetectedEvent : TelemetryEventRaw < EventKeylessEnvDriftPayload > = {
138
+ event : EVENT_KEYLESS_ENV_DRIFT_DETECTED ,
139
+ eventSamplingRate : EVENT_SAMPLING_RATE ,
140
+ payload,
141
+ } ;
142
+
143
+ clerkClient . telemetry ?. record ( driftDetectedEvent ) ;
144
+ }
90
145
}
91
146
} catch ( error ) {
92
147
// Silently handle errors to avoid breaking the application
0 commit comments