Skip to content

Commit f44ad77

Browse files
committed
Merge branch 'analytics-event-tracking' of https://github.com/stack-auth/stack-auth into analytics-event-tracking
2 parents 97055c9 + fa09166 commit f44ad77

24 files changed

Lines changed: 1463 additions & 53 deletions

File tree

AGENTS.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ You should ALWAYS add new E2E tests when you change the API or SDK interface. Ge
2323
- **Run some tests**: `pnpm test run <file-filters>`
2424

2525
### Database Commands
26-
- **Generate migration**: `pnpm db:migration-gen`
26+
- **Generate migration**: `pnpm db:migration-gen` — NOTE: don't forget to create tests for the migrations!
2727
- **Reset database** (rarely used): `pnpm db:reset`
2828
- **Seed database** (rarely used): `pnpm db:seed`
2929
- **Initialize database** (rarely used): `pnpm db:init`
@@ -100,8 +100,10 @@ To see all development ports, refer to the index.html of `apps/dev-launchpad/pub
100100
- NEVER implement a hacky solution without EXPLICIT approval from the user. Always go the extra mile to make sure the solution is clean, maintainable, and robust.
101101
- Fail early, fail loud. Fail fast with an error instead of silently continuing.
102102
- Do NOT use `as`/`any`/type casts or anything else like that to bypass the type system unless you specifically asked the user about it. Most of the time a place where you would use type casts is not one where you actually need them. Avoid wherever possible.
103-
- When writing database migration files, assume that we have >1,000,000 rows in every table (unless otherwise specified). This means you may have to use CONDITIONALLY_REPEAT_MIGRATION_SENTINEL to avoid running the migration and things like concurrent index builds; see the existing migrations for examples.
103+
- When writing database migration files, assume that we have >1,000,000 rows in every table (unless otherwise specified). This means you may have to use CONDITIONALLY_REPEAT_MIGRATION_SENTINEL to avoid running the migration and things like concurrent index builds; see the existing migrations for examples. One common pattern is to add a temporary extra boolean column
104104
- Each migration file runs in its own transaction with a relatively short timeout. Split long-running operations into separate migration files to avoid timeouts. For example, when adding CHECK constraints, use `NOT VALID` in one migration, then `VALIDATE CONSTRAINT` in a separate migration file.
105+
- Note that each database migration file is executed in a single transaction. Even with the run-outside-transaction sentinel, the transaction will still continue during the entire migration file. If you want to split things up into multiple transactions, put it into their own migration files.
106+
- When writing database migration files, ALWAYS ALWAYS add tests for all the potential edge cases! See the folder structure of the other migrations to see how that works.
105107
- **When building frontend code, always carefully deal with loading and error states.** Be very explicit with these; some components make this easy, eg. the button onClick already takes an async callback for loading state, but make sure this is done everywhere, and make sure errors are NEVER just silently swallowed.
106108
- Any design components you add or modify in the dashboard, update the Playground page accordingly to showcase the changes.
107109
- Unless very clearly equivalent from types, prefer explicit null/undefinedness checks over boolean checks, eg. `foo == null` instead of `!foo`.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { randomUUID } from 'crypto';
2+
import type { Sql } from 'postgres';
3+
import { expect } from 'vitest';
4+
5+
export const preMigration = async (sql: Sql) => {
6+
const projectId = `test-${randomUUID()}`;
7+
const tenancyId = randomUUID();
8+
const userId1 = randomUUID();
9+
const userId2 = randomUUID();
10+
11+
await sql`INSERT INTO "Project" ("id", "createdAt", "updatedAt", "displayName", "description", "isProductionMode") VALUES (${projectId}, NOW(), NOW(), 'Test', '', false)`;
12+
await sql`INSERT INTO "Tenancy" ("id", "createdAt", "updatedAt", "projectId", "branchId", "hasNoOrganization") VALUES (${tenancyId}::uuid, NOW(), NOW(), ${projectId}, 'main', 'TRUE'::"BooleanTrue")`;
13+
await sql`INSERT INTO "ProjectUser" ("projectUserId", "tenancyId", "mirroredProjectId", "mirroredBranchId", "createdAt", "updatedAt", "lastActiveAt") VALUES (${userId1}::uuid, ${tenancyId}::uuid, ${projectId}, 'main', NOW(), NOW(), NOW())`;
14+
await sql`INSERT INTO "ProjectUser" ("projectUserId", "tenancyId", "mirroredProjectId", "mirroredBranchId", "createdAt", "updatedAt", "lastActiveAt") VALUES (${userId2}::uuid, ${tenancyId}::uuid, ${projectId}, 'main', NOW(), NOW(), NOW())`;
15+
16+
return { projectId };
17+
};
18+
19+
export const postMigration = async (sql: Sql, ctx: Awaited<ReturnType<typeof preMigration>>) => {
20+
const rows = await sql`
21+
SELECT "restrictedByAdmin", "restrictedByAdminReason"
22+
FROM "ProjectUser"
23+
WHERE "mirroredProjectId" = ${ctx.projectId}
24+
`;
25+
26+
expect(rows).toHaveLength(2);
27+
for (const row of rows) {
28+
expect(row.restrictedByAdmin).toBe(false);
29+
expect(row.restrictedByAdminReason).toBeNull();
30+
}
31+
};
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { randomUUID } from 'crypto';
2+
import type { Sql } from 'postgres';
3+
import { expect } from 'vitest';
4+
5+
export const preMigration = async (sql: Sql) => {
6+
const projectId = `test-${randomUUID()}`;
7+
const tenancyId = randomUUID();
8+
const unrestricted = randomUUID();
9+
const restrictedWithReason = randomUUID();
10+
const restrictedNoReason = randomUUID();
11+
12+
await sql`INSERT INTO "Project" ("id", "createdAt", "updatedAt", "displayName", "description", "isProductionMode") VALUES (${projectId}, NOW(), NOW(), 'Test', '', false)`;
13+
await sql`INSERT INTO "Tenancy" ("id", "createdAt", "updatedAt", "projectId", "branchId", "hasNoOrganization") VALUES (${tenancyId}::uuid, NOW(), NOW(), ${projectId}, 'main', 'TRUE'::"BooleanTrue")`;
14+
15+
// Unrestricted user (valid: false + null reason)
16+
await sql`INSERT INTO "ProjectUser" ("projectUserId", "tenancyId", "mirroredProjectId", "mirroredBranchId", "createdAt", "updatedAt", "lastActiveAt") VALUES (${unrestricted}::uuid, ${tenancyId}::uuid, ${projectId}, 'main', NOW(), NOW(), NOW())`;
17+
18+
// Restricted with reason
19+
await sql`INSERT INTO "ProjectUser" ("projectUserId", "tenancyId", "mirroredProjectId", "mirroredBranchId", "createdAt", "updatedAt", "lastActiveAt", "restrictedByAdmin", "restrictedByAdminReason") VALUES (${restrictedWithReason}::uuid, ${tenancyId}::uuid, ${projectId}, 'main', NOW(), NOW(), NOW(), true, 'spam')`;
20+
21+
// Restricted without reason
22+
await sql`INSERT INTO "ProjectUser" ("projectUserId", "tenancyId", "mirroredProjectId", "mirroredBranchId", "createdAt", "updatedAt", "lastActiveAt", "restrictedByAdmin") VALUES (${restrictedNoReason}::uuid, ${tenancyId}::uuid, ${projectId}, 'main', NOW(), NOW(), NOW(), true)`;
23+
24+
return { projectId, tenancyId, unrestricted, restrictedWithReason, restrictedNoReason };
25+
};
26+
27+
export const postMigration = async (sql: Sql, ctx: Awaited<ReturnType<typeof preMigration>>) => {
28+
// Existing valid rows should still be there
29+
const rows = await sql`
30+
SELECT "projectUserId", "restrictedByAdmin", "restrictedByAdminReason", "restrictedByAdminPrivateDetails"
31+
FROM "ProjectUser"
32+
WHERE "mirroredProjectId" = ${ctx.projectId}
33+
ORDER BY "projectUserId"
34+
`;
35+
expect(rows).toHaveLength(3);
36+
37+
for (const row of rows) {
38+
expect(row.restrictedByAdminPrivateDetails).toBeNull();
39+
}
40+
41+
// Restricted user can have private details set
42+
await sql`UPDATE "ProjectUser" SET "restrictedByAdminPrivateDetails" = 'internal notes' WHERE "projectUserId" = ${ctx.restrictedWithReason}::uuid`;
43+
44+
// INVALID: unrestricted user with a reason should fail
45+
await expect(sql`
46+
UPDATE "ProjectUser" SET "restrictedByAdminReason" = 'should fail' WHERE "projectUserId" = ${ctx.unrestricted}::uuid
47+
`).rejects.toThrow(/ProjectUser_restricted_by_admin_consistency/);
48+
49+
// INVALID: unrestricted user with private details should fail
50+
await expect(sql`
51+
UPDATE "ProjectUser" SET "restrictedByAdminPrivateDetails" = 'should fail' WHERE "projectUserId" = ${ctx.unrestricted}::uuid
52+
`).rejects.toThrow(/ProjectUser_restricted_by_admin_consistency/);
53+
54+
// VALID: new restricted user with all fields
55+
const newUser = randomUUID();
56+
await sql`INSERT INTO "ProjectUser" ("projectUserId", "tenancyId", "mirroredProjectId", "mirroredBranchId", "createdAt", "updatedAt", "lastActiveAt", "restrictedByAdmin", "restrictedByAdminReason", "restrictedByAdminPrivateDetails") VALUES (${newUser}::uuid, ${ctx.tenancyId}::uuid, ${ctx.projectId}, 'main', NOW(), NOW(), NOW(), true, 'test', 'details')`;
57+
58+
// VALID: un-restricting clears reason and details
59+
await sql`UPDATE "ProjectUser" SET "restrictedByAdmin" = false, "restrictedByAdminReason" = NULL, "restrictedByAdminPrivateDetails" = NULL WHERE "projectUserId" = ${newUser}::uuid`;
60+
};
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { randomUUID } from 'crypto';
2+
import type { Sql } from 'postgres';
3+
import { expect } from 'vitest';
4+
5+
export const preMigration = async (sql: Sql) => {
6+
const dedupKey = `dedup-${randomUUID()}`;
7+
const fulfilledKey = `fulfilled-${randomUUID()}`;
8+
9+
// Pending request
10+
await sql`INSERT INTO "OutgoingRequest" ("id", "deduplicationKey", "qstashOptions") VALUES (${randomUUID()}::uuid, ${dedupKey}, '{"url":"http://test"}'::jsonb)`;
11+
12+
// Fulfilled request with a different key
13+
await sql`INSERT INTO "OutgoingRequest" ("id", "deduplicationKey", "qstashOptions", "startedFulfillingAt") VALUES (${randomUUID()}::uuid, ${fulfilledKey}, '{"url":"http://test"}'::jsonb, NOW())`;
14+
15+
return { dedupKey, fulfilledKey };
16+
};
17+
18+
export const postMigration = async (sql: Sql, ctx: Awaited<ReturnType<typeof preMigration>>) => {
19+
// Duplicate pending requests should still be rejected
20+
await expect(sql`
21+
INSERT INTO "OutgoingRequest" ("id", "deduplicationKey", "qstashOptions") VALUES (${randomUUID()}::uuid, ${ctx.dedupKey}, '{"url":"http://test2"}'::jsonb)
22+
`).rejects.toThrow(/OutgoingRequest_deduplicationKey_pending_key/);
23+
24+
// Fulfill the original pending request
25+
await sql`UPDATE "OutgoingRequest" SET "startedFulfillingAt" = NOW() WHERE "deduplicationKey" = ${ctx.dedupKey}`;
26+
27+
// Now we CAN insert a new pending request with the same dedup key
28+
await sql`INSERT INTO "OutgoingRequest" ("id", "deduplicationKey", "qstashOptions") VALUES (${randomUUID()}::uuid, ${ctx.dedupKey}, '{"url":"http://test3"}'::jsonb)`;
29+
30+
// Fulfilled requests can share dedup keys freely
31+
await sql`INSERT INTO "OutgoingRequest" ("id", "deduplicationKey", "qstashOptions", "startedFulfillingAt") VALUES (${randomUUID()}::uuid, ${ctx.fulfilledKey}, '{"url":"http://test4"}'::jsonb, NOW())`;
32+
33+
const pending = await sql`SELECT COUNT(*) as count FROM "OutgoingRequest" WHERE "deduplicationKey" = ${ctx.dedupKey} AND "startedFulfillingAt" IS NULL`;
34+
expect(Number(pending[0].count)).toBe(1);
35+
36+
const fulfilled = await sql`SELECT COUNT(*) as count FROM "OutgoingRequest" WHERE "deduplicationKey" = ${ctx.dedupKey} AND "startedFulfillingAt" IS NOT NULL`;
37+
expect(Number(fulfilled[0].count)).toBe(1);
38+
};
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { randomUUID } from 'crypto';
2+
import type { Sql } from 'postgres';
3+
import { expect } from 'vitest';
4+
5+
export const preMigration = async (sql: Sql) => {
6+
const projectId = `test-${randomUUID()}`;
7+
const domainId = randomUUID();
8+
9+
await sql`INSERT INTO "Project" ("id", "createdAt", "updatedAt", "displayName", "description", "isProductionMode") VALUES (${projectId}, NOW(), NOW(), 'Test', '', false)`;
10+
11+
// Config that already has both parent AND child keys (correct format)
12+
const config = {
13+
[`domains.trustedDomains.${domainId}`]: {},
14+
[`domains.trustedDomains.${domainId}.baseUrl`]: 'https://correct.com',
15+
[`domains.trustedDomains.${domainId}.handlerPath`]: '/api',
16+
'some.other.key': 'untouched',
17+
};
18+
await sql`INSERT INTO "EnvironmentConfigOverride" ("projectId", "branchId", "createdAt", "updatedAt", "config") VALUES (${projectId}, 'main', NOW(), NOW(), ${sql.json(config)})`;
19+
20+
return { projectId, domainId };
21+
};
22+
23+
export const postMigration = async (sql: Sql, ctx: Awaited<ReturnType<typeof preMigration>>) => {
24+
const [row] = await sql`SELECT "config" FROM "EnvironmentConfigOverride" WHERE "projectId" = ${ctx.projectId}`;
25+
26+
expect(row.config[`domains.trustedDomains.${ctx.domainId}`]).toEqual({});
27+
expect(row.config[`domains.trustedDomains.${ctx.domainId}.baseUrl`]).toBe('https://correct.com');
28+
expect(row.config[`domains.trustedDomains.${ctx.domainId}.handlerPath`]).toBe('/api');
29+
expect(row.config['some.other.key']).toBe('untouched');
30+
};
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { randomUUID } from 'crypto';
2+
import type { Sql } from 'postgres';
3+
import { expect } from 'vitest';
4+
5+
export const preMigration = async (sql: Sql) => {
6+
const projectId = `test-${randomUUID()}`;
7+
const domainId = randomUUID();
8+
9+
await sql`INSERT INTO "Project" ("id", "createdAt", "updatedAt", "displayName", "description", "isProductionMode") VALUES (${projectId}, NOW(), NOW(), 'Test', '', false)`;
10+
11+
// Config with child keys but MISSING parent key
12+
const config = {
13+
[`domains.trustedDomains.${domainId}.baseUrl`]: 'https://example.com',
14+
[`domains.trustedDomains.${domainId}.handlerPath`]: '/handler',
15+
};
16+
await sql`INSERT INTO "EnvironmentConfigOverride" ("projectId", "branchId", "createdAt", "updatedAt", "config") VALUES (${projectId}, 'main', NOW(), NOW(), ${sql.json(config)})`;
17+
18+
return { projectId, domainId };
19+
};
20+
21+
export const postMigration = async (sql: Sql, ctx: Awaited<ReturnType<typeof preMigration>>) => {
22+
const [row] = await sql`SELECT "config" FROM "EnvironmentConfigOverride" WHERE "projectId" = ${ctx.projectId}`;
23+
const parentKey = `domains.trustedDomains.${ctx.domainId}`;
24+
25+
// Parent key should now exist as an empty object
26+
expect(row.config).toHaveProperty(parentKey);
27+
expect(row.config[parentKey]).toEqual({});
28+
29+
// Child keys should still be present and unchanged
30+
expect(row.config[`${parentKey}.baseUrl`]).toBe('https://example.com');
31+
expect(row.config[`${parentKey}.handlerPath`]).toBe('/handler');
32+
};
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { randomUUID } from 'crypto';
2+
import type { Sql } from 'postgres';
3+
import { expect } from 'vitest';
4+
5+
export const preMigration = async (sql: Sql) => {
6+
const projectId1 = `test-${randomUUID()}`;
7+
const projectId2 = `test-${randomUUID()}`;
8+
const domainOk = randomUUID();
9+
const domainBroken = randomUUID();
10+
11+
await sql`INSERT INTO "Project" ("id", "createdAt", "updatedAt", "displayName", "description", "isProductionMode") VALUES (${projectId1}, NOW(), NOW(), 'Test1', '', false)`;
12+
await sql`INSERT INTO "Project" ("id", "createdAt", "updatedAt", "displayName", "description", "isProductionMode") VALUES (${projectId2}, NOW(), NOW(), 'Test2', '', false)`;
13+
14+
// Project 1: correctly formatted (parent key present)
15+
const config1 = {
16+
[`domains.trustedDomains.${domainOk}`]: {},
17+
[`domains.trustedDomains.${domainOk}.baseUrl`]: 'https://ok.com',
18+
};
19+
await sql`INSERT INTO "EnvironmentConfigOverride" ("projectId", "branchId", "createdAt", "updatedAt", "config") VALUES (${projectId1}, 'main', NOW(), NOW(), ${sql.json(config1)})`;
20+
21+
// Project 2: broken (parent key missing)
22+
const config2 = {
23+
[`domains.trustedDomains.${domainBroken}.baseUrl`]: 'https://broken.com',
24+
};
25+
await sql`INSERT INTO "EnvironmentConfigOverride" ("projectId", "branchId", "createdAt", "updatedAt", "config") VALUES (${projectId2}, 'main', NOW(), NOW(), ${sql.json(config2)})`;
26+
27+
return { projectId1, projectId2, domainOk, domainBroken };
28+
};
29+
30+
export const postMigration = async (sql: Sql, ctx: Awaited<ReturnType<typeof preMigration>>) => {
31+
// Project 1: unchanged
32+
const [row1] = await sql`SELECT "config" FROM "EnvironmentConfigOverride" WHERE "projectId" = ${ctx.projectId1}`;
33+
expect(row1.config[`domains.trustedDomains.${ctx.domainOk}`]).toEqual({});
34+
expect(row1.config[`domains.trustedDomains.${ctx.domainOk}.baseUrl`]).toBe('https://ok.com');
35+
36+
// Project 2: parent key added
37+
const [row2] = await sql`SELECT "config" FROM "EnvironmentConfigOverride" WHERE "projectId" = ${ctx.projectId2}`;
38+
expect(row2.config[`domains.trustedDomains.${ctx.domainBroken}`]).toEqual({});
39+
expect(row2.config[`domains.trustedDomains.${ctx.domainBroken}.baseUrl`]).toBe('https://broken.com');
40+
};
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { randomUUID } from 'crypto';
2+
import type { Sql } from 'postgres';
3+
import { expect } from 'vitest';
4+
5+
export const preMigration = async (sql: Sql) => {
6+
const projectId = `test-${randomUUID()}`;
7+
const domainId1 = randomUUID();
8+
const domainId2 = randomUUID();
9+
10+
await sql`INSERT INTO "Project" ("id", "createdAt", "updatedAt", "displayName", "description", "isProductionMode") VALUES (${projectId}, NOW(), NOW(), 'Test', '', false)`;
11+
12+
// Two different domains, both missing parent keys
13+
const config = {
14+
[`domains.trustedDomains.${domainId1}.baseUrl`]: 'https://one.com',
15+
[`domains.trustedDomains.${domainId1}.handlerPath`]: '/one',
16+
[`domains.trustedDomains.${domainId2}.baseUrl`]: 'https://two.com',
17+
[`domains.trustedDomains.${domainId2}.handlerPath`]: '/two',
18+
[`domains.trustedDomains.${domainId2}.extra`]: 'data',
19+
};
20+
await sql`INSERT INTO "EnvironmentConfigOverride" ("projectId", "branchId", "createdAt", "updatedAt", "config") VALUES (${projectId}, 'main', NOW(), NOW(), ${sql.json(config)})`;
21+
22+
return { projectId, domainId1, domainId2 };
23+
};
24+
25+
export const postMigration = async (sql: Sql, ctx: Awaited<ReturnType<typeof preMigration>>) => {
26+
const [row] = await sql`SELECT "config" FROM "EnvironmentConfigOverride" WHERE "projectId" = ${ctx.projectId}`;
27+
28+
// Both parent keys should be added
29+
expect(row.config[`domains.trustedDomains.${ctx.domainId1}`]).toEqual({});
30+
expect(row.config[`domains.trustedDomains.${ctx.domainId2}`]).toEqual({});
31+
32+
// All child keys preserved
33+
expect(row.config[`domains.trustedDomains.${ctx.domainId1}.baseUrl`]).toBe('https://one.com');
34+
expect(row.config[`domains.trustedDomains.${ctx.domainId1}.handlerPath`]).toBe('/one');
35+
expect(row.config[`domains.trustedDomains.${ctx.domainId2}.baseUrl`]).toBe('https://two.com');
36+
expect(row.config[`domains.trustedDomains.${ctx.domainId2}.handlerPath`]).toBe('/two');
37+
expect(row.config[`domains.trustedDomains.${ctx.domainId2}.extra`]).toBe('data');
38+
};
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { randomUUID } from 'crypto';
2+
import type { Sql } from 'postgres';
3+
import { expect } from 'vitest';
4+
5+
export const preMigration = async (sql: Sql) => {
6+
const projectId = `test-${randomUUID()}`;
7+
8+
await sql`INSERT INTO "Project" ("id", "createdAt", "updatedAt", "displayName", "description", "isProductionMode") VALUES (${projectId}, NOW(), NOW(), 'Test', '', false)`;
9+
10+
const config = { 'auth.allowSignUp': true, 'payments.testMode': true, 'some.nested.value': 42 };
11+
await sql`INSERT INTO "EnvironmentConfigOverride" ("projectId", "branchId", "createdAt", "updatedAt", "config") VALUES (${projectId}, 'main', NOW(), NOW(), ${sql.json(config)})`;
12+
13+
return { projectId, originalConfig: config };
14+
};
15+
16+
export const postMigration = async (sql: Sql, ctx: Awaited<ReturnType<typeof preMigration>>) => {
17+
const [row] = await sql`SELECT "config" FROM "EnvironmentConfigOverride" WHERE "projectId" = ${ctx.projectId}`;
18+
expect(row.config).toEqual(ctx.originalConfig);
19+
};
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import type { Sql } from 'postgres';
2+
import { expect } from 'vitest';
3+
4+
// No preMigration needed - we just verify cleanup after the migration runs
5+
6+
export const postMigration = async (sql: Sql) => {
7+
// Temporary column should NOT exist after migration
8+
const columns = await sql`
9+
SELECT column_name FROM information_schema.columns
10+
WHERE table_name = 'EnvironmentConfigOverride'
11+
AND column_name = 'temp_trusted_domains_checked'
12+
`;
13+
expect(columns).toHaveLength(0);
14+
15+
// Temporary index should NOT exist after migration
16+
const indices = await sql`
17+
SELECT indexname FROM pg_indexes
18+
WHERE indexname = 'temp_eco_trusted_domains_checked_idx'
19+
`;
20+
expect(indices).toHaveLength(0);
21+
};

0 commit comments

Comments
 (0)