11import type { TelemetryEventRaw } from '@clerk/types' ;
2+ import { promises as fs } from 'fs' ;
3+ import { join } from 'path' ;
24
35import { createClerkClientWithOptions } from './createClerkClient' ;
46
57const EVENT_KEYLESS_ENV_DRIFT_DETECTED = 'KEYLESS_ENV_DRIFT_DETECTED' ;
68const EVENT_SAMPLING_RATE = 1 ; // 100% sampling rate
9+ const TELEMETRY_FLAG_FILE = '.clerk/.tmp/telemetry.json' ;
710
811type EventKeylessEnvDriftPayload = {
912 publicKeyMatch : boolean ;
@@ -15,14 +18,61 @@ type EventKeylessEnvDriftPayload = {
1518} ;
1619
1720/**
18- * Detects environment variable drift for keyless Next.js applications and fires telemetry events .
21+ * Gets the absolute path to the telemetry flag file .
1922 *
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 .
2225 *
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
2676 */
2777export async function detectKeylessEnvDrift ( ) : Promise < void > {
2878 // Only run on server side
@@ -55,10 +105,11 @@ export async function detectKeylessEnvDrift(): Promise<void> {
55105 }
56106
57107 // Compare publishable keys
58- const publicKeyMatch = Boolean ( envPublishableKey === keylessFile ?. publishableKey ) ;
59-
108+ const publicKeyMatch = Boolean (
109+ envPublishableKey && keylessFile ?. publishableKey && envPublishableKey === keylessFile . publishableKey ,
110+ ) ;
60111 // Compare secret keys
61- const secretKeyMatch = Boolean ( envSecretKey === keylessFile ? .secretKey ) ;
112+ const secretKeyMatch = Boolean ( envSecretKey && keylessFile ?. secretKey && envSecretKey === keylessFile . secretKey ) ;
62113
63114 // Check if there's a drift (mismatch between env vars and keyless file)
64115 const hasDrift = ! publicKeyMatch || ! secretKeyMatch ;
@@ -69,7 +120,7 @@ export async function detectKeylessEnvDrift(): Promise<void> {
69120 envVarsMissing,
70121 keylessFileHasKeys,
71122 keylessPublishableKey : keylessFile . publishableKey ,
72- envPublishableKey : envPublishableKey as string ,
123+ envPublishableKey : envPublishableKey ?? '' ,
73124 } ;
74125
75126 // Create a clerk client to access telemetry
@@ -79,14 +130,18 @@ export async function detectKeylessEnvDrift(): Promise<void> {
79130 } ) ;
80131
81132 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+ }
90145 }
91146 } catch ( error ) {
92147 // Silently handle errors to avoid breaking the application
0 commit comments