Skip to content

Commit a7480c6

Browse files
authored
Merge branch 'dev' into managed-emails-provider
2 parents 78d8e5d + 53c1c9e commit a7480c6

5 files changed

Lines changed: 92 additions & 6 deletions

File tree

.cursor/commands/repro-fix.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Reproduce the bug and write a test for it, then fix it.

apps/backend/src/app/api/latest/internal/sign-up-rules-stats/route.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,10 @@ export const GET = createSmartRouteHandler({
5555
const result = await client.query({
5656
query: `
5757
SELECT
58-
data.ruleId as rule_id,
58+
COALESCE(
59+
NULLIF(CAST(data.rule_id, 'Nullable(String)'), ''),
60+
NULLIF(CAST(data.ruleId, 'Nullable(String)'), '')
61+
) as rule_id,
5962
data.action as action,
6063
toStartOfHour(event_at) as hour
6164
FROM analytics_internal.events
@@ -72,12 +75,14 @@ export const GET = createSmartRouteHandler({
7275
},
7376
format: "JSONEachRow",
7477
});
75-
const rows: {
76-
rule_id: string,
78+
const rawRows: {
79+
rule_id: string | null,
7780
action: "allow" | "reject" | "restrict" | "log",
7881
hour: string,
7982
}[] = await result.json();
8083

84+
const rows = rawRows.filter((row): row is typeof row & { rule_id: string } => row.rule_id != null && row.rule_id !== '');
85+
8186
// Group by rule and hour for sparkline data
8287
const ruleTriggersMap = new Map<string, {
8388
totalCount: number,

apps/backend/src/lib/events.tsx

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -310,10 +310,36 @@ export async function logEvent<T extends EventType[]>(
310310
is_anonymous: isAnonymous,
311311
ip_info: toClickhouseEndUserIpInfo(ipInfo ?? null),
312312
};
313-
} else {
313+
} else if (matchingEventType.id === "$sign-up-rule-trigger") {
314+
const ruleId =
315+
typeof dataRecord === "object" && dataRecord && typeof dataRecord.ruleId === "string"
316+
? dataRecord.ruleId
317+
: throwErr(new StackAssertionError("ruleId is required for $sign-up-rule-trigger ClickHouse event", { dataRecord }));
318+
const action =
319+
typeof dataRecord === "object" && dataRecord && typeof dataRecord.action === "string"
320+
? dataRecord.action
321+
: throwErr(new StackAssertionError("action is required for $sign-up-rule-trigger ClickHouse event", { dataRecord }));
322+
const email =
323+
typeof dataRecord === "object" && dataRecord
324+
? (dataRecord.email as string | null | undefined) ?? null
325+
: null;
326+
const authMethod =
327+
typeof dataRecord === "object" && dataRecord
328+
? (dataRecord.authMethod as string | null | undefined) ?? null
329+
: null;
330+
const oauthProvider =
331+
typeof dataRecord === "object" && dataRecord
332+
? (dataRecord.oauthProvider as string | null | undefined) ?? null
333+
: null;
314334
clickhouseEventData = {
315-
...(data as Record<string, unknown>),
335+
rule_id: ruleId,
336+
action,
337+
email,
338+
auth_method: authMethod,
339+
oauth_provider: oauthProvider,
316340
};
341+
} else {
342+
throw new StackAssertionError(`Unhandled ClickHouse event type: ${matchingEventType.id}`, { matchingEventType });
317343
}
318344

319345
if (!projectId) {

apps/e2e/tests/backend/endpoints/api/v1/internal-metrics.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ it("should return metrics data with users", async ({ expect }) => {
8181

8282
await ensureAnonymousUsersAreStillExcluded(response);
8383
}, {
84-
timeout: 120_000,
84+
timeout: 240_000,
8585
});
8686

8787
it("should not work for non-admins", async ({ expect }) => {

apps/e2e/tests/backend/endpoints/api/v1/internal/sign-up-rules-stats.test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,4 +169,58 @@ describe("with admin access", () => {
169169
expect(lastHourlyCount.hour).toEqual(new Date().toISOString().slice(0, 13) + ':00:00.000Z');
170170
expect(lastHourlyCount.count).toBe(1);
171171
});
172+
173+
it("should read rule_id from ClickHouse events with COALESCE for both camelCase and snake_case data", async ({ expect }) => {
174+
// Regression test: a ClickHouse migration converts ruleId -> rule_id (snake_case).
175+
// The stats query must handle both field name formats via COALESCE.
176+
// This test verifies the COALESCE query reads the correct rule_id from the event.
177+
await Project.createAndSwitch();
178+
await Project.updateConfig({
179+
'auth.signUpRules.coalesce-rule': {
180+
enabled: true,
181+
displayName: 'COALESCE Test Rule',
182+
priority: 1,
183+
condition: 'true',
184+
action: { type: 'log' },
185+
},
186+
});
187+
188+
await Auth.Password.signUpWithEmail();
189+
190+
// Wait for the ClickHouse event to appear and verify via a raw COALESCE query
191+
let chResult: any;
192+
for (let attempt = 0; attempt < 15; attempt++) {
193+
await wait(500);
194+
chResult = await niceBackendFetch("/api/v1/internal/analytics/query", {
195+
method: "POST",
196+
accessType: "admin",
197+
body: {
198+
query: `
199+
SELECT
200+
COALESCE(
201+
NULLIF(CAST(data.rule_id, 'Nullable(String)'), ''),
202+
NULLIF(CAST(data.ruleId, 'Nullable(String)'), '')
203+
) as rule_id
204+
FROM events
205+
WHERE event_type = '$sign-up-rule-trigger'
206+
LIMIT 1
207+
`,
208+
params: {},
209+
},
210+
});
211+
if (chResult.status === 200 && chResult.body?.result?.length > 0) break;
212+
}
213+
214+
expect(chResult.status).toBe(200);
215+
expect(chResult.body.result.length).toBeGreaterThan(0);
216+
expect(chResult.body.result[0].rule_id).toBe('coalesce-rule');
217+
218+
// Verify the stats endpoint returns correct data with the COALESCE-based query
219+
const response = await niceBackendFetch("/api/v1/internal/sign-up-rules-stats", { accessType: "admin" });
220+
expect(response.status).toBe(200);
221+
expect(response.body.rule_triggers.length).toBeGreaterThan(0);
222+
const trigger = response.body.rule_triggers.find((t: any) => t.rule_id === 'coalesce-rule');
223+
expect(trigger).toBeTruthy();
224+
expect(trigger.total_count).toBeGreaterThanOrEqual(1);
225+
});
172226
});

0 commit comments

Comments
 (0)