Skip to content

Commit 3719ab0

Browse files
Telemetry event for keyless env drift
1 parent 173837c commit 3719ab0

File tree

3 files changed

+105
-0
lines changed

3 files changed

+105
-0
lines changed

packages/nextjs/src/app-router/keyless-actions.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { redirect, RedirectType } from 'next/navigation';
66
import { errorThrower } from '../server/errorThrower';
77
import { detectClerkMiddleware } from '../server/headers-utils';
88
import { getKeylessCookieName, getKeylessCookieValue } from '../server/keyless';
9+
import { detectKeylessEnvDrift } from '../server/keyless-telemetry';
910
import { canUseKeyless } from '../utils/feature-flags';
1011

1112
type SetCookieOptions = Parameters<Awaited<ReturnType<typeof cookies>>['set']>[2];
@@ -61,6 +62,9 @@ export async function createOrReadKeylessAction(): Promise<null | Omit<Accountle
6162
return null;
6263
}
6364

65+
// Detect environment variable drift and fire telemetry events
66+
await detectKeylessEnvDrift();
67+
6468
const { clerkDevelopmentCache, createKeylessModeMessage } = await import('../server/keyless-log-cache.js');
6569

6670
/**

packages/nextjs/src/app-router/server/keyless-provider.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import React from 'react';
66

77
import { createClerkClientWithOptions } from '../../server/createClerkClient';
88
import { collectKeylessMetadata, formatMetadataHeaders } from '../../server/keyless-custom-headers';
9+
import { detectKeylessEnvDrift } from '../../server/keyless-telemetry';
910
import type { NextClerkProviderProps } from '../../types';
1011
import { canUseKeyless } from '../../utils/feature-flags';
1112
import { mergeNextClerkPropsWithEnv } from '../../utils/mergeNextClerkPropsWithEnv';
@@ -47,6 +48,11 @@ export const KeylessProvider = async (props: KeylessProviderProps) => {
4748
.then(mod => mod.createOrReadKeyless())
4849
.catch(() => null);
4950

51+
// Detect environment variable drift and fire telemetry events
52+
if (newOrReadKeys) {
53+
await detectKeylessEnvDrift();
54+
}
55+
5056
const { clerkDevelopmentCache, createConfirmationMessage, createKeylessModeMessage } = await import(
5157
'../../server/keyless-log-cache.js'
5258
);
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import type { TelemetryEventRaw } from '@clerk/types';
2+
3+
import { createClerkClientWithOptions } from './createClerkClient';
4+
5+
const EVENT_KEYLESS_ENV_DRIFT_DETECTED = 'KEYLESS_ENV_DRIFT_DETECTED';
6+
const EVENT_SAMPLING_RATE = 1; // 100% sampling rate
7+
8+
type EventKeylessEnvDriftPayload = {
9+
publicKeyMatch: boolean;
10+
secretKeyMatch: boolean;
11+
envVarsMissing: boolean;
12+
keylessFileHasKeys: boolean;
13+
keylessPublishableKey: string;
14+
envPublishableKey: string;
15+
};
16+
17+
/**
18+
* Detects environment variable drift for keyless Next.js applications and fires telemetry events.
19+
*
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.
22+
*
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+
*/
27+
export async function detectKeylessEnvDrift(): Promise<void> {
28+
// Only run on server side
29+
if (typeof window !== 'undefined') {
30+
return;
31+
}
32+
33+
try {
34+
// Dynamically import server-side dependencies to avoid client-side issues
35+
const { safeParseClerkFile } = await import('./keyless-node.js');
36+
37+
// Read the keyless configuration file
38+
const keylessFile = safeParseClerkFile();
39+
40+
if (!keylessFile) {
41+
return;
42+
}
43+
44+
// Get environment variables
45+
const envPublishableKey = process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY;
46+
const envSecretKey = process.env.CLERK_SECRET_KEY;
47+
48+
// Check if environment variables are missing, and keys exist in keyless file
49+
const envVarsMissing = !envPublishableKey && !envSecretKey;
50+
const keylessFileHasKeys = Boolean(keylessFile?.publishableKey && keylessFile?.secretKey);
51+
52+
if (envVarsMissing && keylessFileHasKeys) {
53+
// Environment variables are missing but keyless file has keys - this is normal for keyless mode
54+
return;
55+
}
56+
57+
// Compare publishable keys
58+
const publicKeyMatch = Boolean(envPublishableKey === keylessFile?.publishableKey);
59+
60+
// Compare secret keys
61+
const secretKeyMatch = Boolean(envSecretKey === keylessFile?.secretKey);
62+
63+
// Check if there's a drift (mismatch between env vars and keyless file)
64+
const hasDrift = !publicKeyMatch || !secretKeyMatch;
65+
66+
const payload: EventKeylessEnvDriftPayload = {
67+
publicKeyMatch,
68+
secretKeyMatch,
69+
envVarsMissing,
70+
keylessFileHasKeys,
71+
keylessPublishableKey: keylessFile.publishableKey,
72+
envPublishableKey: envPublishableKey as string,
73+
};
74+
75+
// Create a clerk client to access telemetry
76+
const clerkClient = createClerkClientWithOptions({
77+
publishableKey: keylessFile.publishableKey,
78+
secretKey: keylessFile.secretKey,
79+
});
80+
81+
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);
90+
}
91+
} catch (error) {
92+
// Silently handle errors to avoid breaking the application
93+
console.warn('Failed to detect keyless environment drift:', error);
94+
}
95+
}

0 commit comments

Comments
 (0)