Skip to content

Commit 6370cc8

Browse files
committed
Merge remote-tracking branch 'origin/dev' into new-setup
2 parents fe7fe4c + 2e41fde commit 6370cc8

16 files changed

Lines changed: 261 additions & 87 deletions

File tree

apps/backend/.env

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ STACK_SEED_INTERNAL_PROJECT_USER_PASSWORD=# default user's password, paired with
1515
STACK_SEED_INTERNAL_PROJECT_USER_INTERNAL_ACCESS=# if the default user has access to the internal dashboard project
1616
STACK_SEED_INTERNAL_PROJECT_USER_GITHUB_ID=# add github oauth id to the default user
1717
STACK_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY=# default publishable client key for the internal project
18-
STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY=# default secret server key for the internal project
18+
STACK_INTERNAL_PROJECT_SECRET_SERVER_KEY=# default secret server key for the internal project
1919
STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY=# default super secret admin key for the internal project
2020

2121
# OAuth mock provider settings

apps/backend/.env.development

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ STACK_SEED_INTERNAL_PROJECT_OAUTH_PROVIDERS=github,spotify,google,microsoft
1414
STACK_SEED_INTERNAL_PROJECT_USER_GITHUB_ID=admin@example.com
1515
STACK_SEED_INTERNAL_PROJECT_USER_INTERNAL_ACCESS=true
1616
STACK_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only
17-
STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY=this-secret-server-key-is-for-local-development-only
17+
STACK_INTERNAL_PROJECT_SECRET_SERVER_KEY=this-secret-server-key-is-for-local-development-only
1818
STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY=this-super-secret-admin-key-is-for-local-development-only
1919

2020
STACK_OAUTH_MOCK_URL=http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}14

apps/backend/prisma/seed.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -372,8 +372,8 @@ export async function seed() {
372372
const keySet = {
373373
publishableClientKey: rawPck || throwErr('STACK_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY is not set'),
374374
secretServerKey: isLocalEmulator
375-
? (process.env.STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY ?? null)
376-
: (process.env.STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY || throwErr('STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY is not set')),
375+
? (process.env.STACK_INTERNAL_PROJECT_SECRET_SERVER_KEY ?? null)
376+
: (process.env.STACK_INTERNAL_PROJECT_SECRET_SERVER_KEY || throwErr('STACK_INTERNAL_PROJECT_SECRET_SERVER_KEY is not set')),
377377
superSecretAdminKey: isLocalEmulator
378378
? (process.env.STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY ?? null)
379379
: (process.env.STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY || throwErr('STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY is not set')),

apps/backend/src/app/api/latest/internal/metrics/route.tsx

Lines changed: 39 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import {
2121
MetricsRecentUserSchema,
2222
} from "@stackframe/stack-shared/dist/interface/admin-metrics";
2323
import { captureError, StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors";
24-
import { stringCompare } from "@stackframe/stack-shared/dist/utils/strings";
2524
import { adaptSchema, adminAuthTypeSchema, yupArray, yupNumber, yupObject, yupRecord, yupString } from "@stackframe/stack-shared/dist/schema-fields";
2625
import { userFullInclude, userPrismaToCrud, usersCrudHandlers } from "../../users/crud";
2726

@@ -123,10 +122,10 @@ async function loadUsersByCountry(tenancy: Tenancy, includeAnonymous: boolean =
123122
);
124123
}
125124

126-
// ClickHouse sample size per country. Small enough to keep the event-table
127-
// scan cheap, large enough for the dashboard globe to pick ~1-5 distinct
128-
// avatars per country based on the country's visible area.
129-
const ACTIVE_USERS_BY_COUNTRY_SAMPLE = 8;
125+
// Max live users returned per country. Small enough to keep the Postgres join
126+
// cheap, large enough for the dashboard globe and satellite bubbles to pick a
127+
// few distinct avatars per country.
128+
const ACTIVE_USERS_BY_COUNTRY_LIMIT = 8;
130129
// "Live" window used to classify users as currently active for the globe
131130
// ping layer. Token-refresh fires every few minutes for each open session,
132131
// so a 2-minute window gives a genuine "who's online right now" read while
@@ -145,27 +144,45 @@ async function loadActiveUsersByCountry(
145144
query: `
146145
SELECT
147146
country_code,
148-
groupArraySample({sample:UInt32})(user_id) AS user_ids
147+
groupArray(user_id) AS user_ids
149148
FROM (
150149
SELECT
151-
user_id,
152-
argMax(cc, event_at) AS country_code
150+
country_code,
151+
user_id
153152
FROM (
154153
SELECT
154+
country_code,
155155
user_id,
156-
event_at,
157-
CAST(data.ip_info.country_code, 'Nullable(String)') AS cc,
158-
coalesce(CAST(data.is_anonymous, 'Nullable(UInt8)'), 0) AS is_anonymous
159-
FROM analytics_internal.events
160-
WHERE event_type = '$token-refresh'
161-
AND project_id = {projectId:String}
162-
AND branch_id = {branchId:String}
163-
AND user_id IS NOT NULL
164-
AND event_at >= {since:DateTime}
156+
row_number() OVER (
157+
PARTITION BY country_code
158+
ORDER BY last_event_at DESC, user_id ASC
159+
) AS country_rank
160+
FROM (
161+
SELECT
162+
user_id,
163+
argMax(cc, event_at) AS country_code,
164+
max(event_at) AS last_event_at
165+
FROM (
166+
SELECT
167+
user_id,
168+
event_at,
169+
CAST(data.ip_info.country_code, 'Nullable(String)') AS cc,
170+
coalesce(CAST(data.is_anonymous, 'Nullable(UInt8)'), 0) AS is_anonymous
171+
FROM analytics_internal.events
172+
WHERE event_type = '$token-refresh'
173+
AND project_id = {projectId:String}
174+
AND branch_id = {branchId:String}
175+
AND user_id IS NOT NULL
176+
AND event_at >= {since:DateTime}
177+
)
178+
WHERE cc IS NOT NULL
179+
AND ({includeAnonymous:UInt8} = 1 OR is_anonymous = 0)
180+
GROUP BY user_id
181+
)
182+
WHERE country_code IS NOT NULL
165183
)
166-
WHERE cc IS NOT NULL
167-
AND ({includeAnonymous:UInt8} = 1 OR is_anonymous = 0)
168-
GROUP BY user_id
184+
WHERE country_rank <= {limit:UInt32}
185+
ORDER BY country_code ASC, country_rank ASC
169186
)
170187
WHERE country_code IS NOT NULL
171188
GROUP BY country_code
@@ -175,13 +192,13 @@ async function loadActiveUsersByCountry(
175192
branchId: tenancy.branchId,
176193
includeAnonymous: includeAnonymous ? 1 : 0,
177194
since: formatClickhouseDateTimeParam(since),
178-
sample: ACTIVE_USERS_BY_COUNTRY_SAMPLE,
195+
limit: ACTIVE_USERS_BY_COUNTRY_LIMIT,
179196
},
180197
format: "JSONEachRow",
181198
});
182199
const rows: { country_code: string, user_ids: string[] }[] = await res.json();
183200

184-
// Collect every sampled UUID once so we only hit Postgres with a single
201+
// Collect every selected UUID once so we only hit Postgres with a single
185202
// `IN (...)` lookup, then re-attach them to their country buckets.
186203
const allIds = new Set<string>();
187204
const countryToIds = new Map<string, string[]>();
@@ -232,15 +249,6 @@ async function loadActiveUsersByCountry(
232249
if (user != null) users.push(user);
233250
}
234251
if (users.length > 0) {
235-
// Sort so the response is stable — `groupArraySample()` returns users
236-
// in random order, which is fine for the globe UI but flakes snapshot
237-
// tests. Primary key is `primary_email` (stable across test runs);
238-
// `id` is a tiebreaker for anonymous users where email is null. The
239-
// globe doesn't rely on any particular order.
240-
users.sort((a, b) => {
241-
const emailCmp = stringCompare(a.primary_email ?? "", b.primary_email ?? "");
242-
return emailCmp !== 0 ? emailCmp : stringCompare(a.id, b.id);
243-
});
244252
result[country] = users;
245253
}
246254
}

apps/backend/src/stack.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,6 @@ export function getStackServerApp() {
1818
projectId: 'internal',
1919
tokenStore: null,
2020
publishableClientKey: getEnvVariable('STACK_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY'),
21-
secretServerKey: getEnvVariable('STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY'),
21+
secretServerKey: getEnvVariable('STACK_INTERNAL_PROJECT_SECRET_SERVER_KEY'),
2222
});
2323
}

docker/local-emulator/qemu/cloud-init/emulator/user-data

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ write_files:
118118
cat /mnt/stack-runtime/base.env
119119
cat /mnt/stack-runtime/runtime.env
120120
printf 'STACK_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY=%s\n' "$INTERNAL_PCK"
121-
printf 'STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY=%s\n' "$INTERNAL_SSK"
121+
printf 'STACK_INTERNAL_PROJECT_SECRET_SERVER_KEY=%s\n' "$INTERNAL_SSK"
122122
printf 'STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY=%s\n' "$INTERNAL_SAK"
123123
if [ -n "$EMULATOR_CRON_SECRET" ]; then
124124
printf 'CRON_SECRET=%s\n' "$EMULATOR_CRON_SECRET"
@@ -503,7 +503,7 @@ write_files:
503503
--env-file /etc/stack-build.env \
504504
--env-file /etc/stack-build-computed.env \
505505
-e STACK_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY="$SMOKE_PCK" \
506-
-e STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY="$SMOKE_SSK" \
506+
-e STACK_INTERNAL_PROJECT_SECRET_SERVER_KEY="$SMOKE_SSK" \
507507
-e STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY="$SMOKE_SAK" \
508508
-e STACK_SKIP_MIGRATIONS=true \
509509
-e STACK_SKIP_SEED_SCRIPT=true \
@@ -646,7 +646,7 @@ write_files:
646646

647647
exec docker exec \
648648
-e STACK_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY \
649-
-e STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY \
649+
-e STACK_INTERNAL_PROJECT_SECRET_SERVER_KEY \
650650
-e STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY \
651651
-e CRON_SECRET \
652652
stack /usr/local/bin/rotate-secrets

docker/local-emulator/qemu/run-emulator.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -693,7 +693,7 @@ qga_trigger_fast_rotate() {
693693
fresh_cron="$(openssl rand -hex 32)"
694694
payload=$(
695695
printf 'STACK_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY=%s\n' "$fresh_pck"
696-
printf 'STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY=%s\n' "$fresh_ssk"
696+
printf 'STACK_INTERNAL_PROJECT_SECRET_SERVER_KEY=%s\n' "$fresh_ssk"
697697
printf 'STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY=%s\n' "$fresh_sak"
698698
printf 'CRON_SECRET=%s\n' "$fresh_cron"
699699
)

docker/local-emulator/rotate-secrets.sh

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ if [ -n "${STACK_ROTATE_INPUT:-}" ] && [ -f "$STACK_ROTATE_INPUT" ]; then
3838
fi
3939

4040
for var in STACK_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY \
41-
STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY \
41+
STACK_INTERNAL_PROJECT_SECRET_SERVER_KEY \
4242
STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY \
4343
CRON_SECRET; do
4444
val="${!var:-}"
@@ -56,12 +56,12 @@ mkdir -p "$(dirname "$OUTPUT")"
5656
umask 077
5757
{
5858
printf 'STACK_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY=%s\n' "$STACK_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY"
59-
printf 'STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY=%s\n' "$STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY"
59+
printf 'STACK_INTERNAL_PROJECT_SECRET_SERVER_KEY=%s\n' "$STACK_INTERNAL_PROJECT_SECRET_SERVER_KEY"
6060
printf 'STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY=%s\n' "$STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY"
6161
printf 'CRON_SECRET=%s\n' "$CRON_SECRET"
6262
# Mirror these so process.env lookups in Node match env after restart.
6363
printf 'NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=%s\n' "$STACK_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY"
64-
printf 'STACK_SECRET_SERVER_KEY=%s\n' "$STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY"
64+
printf 'STACK_SECRET_SERVER_KEY=%s\n' "$STACK_INTERNAL_PROJECT_SECRET_SERVER_KEY"
6565
printf 'STACK_SUPER_SECRET_ADMIN_KEY=%s\n' "$STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY"
6666
} > "$OUTPUT"
6767
chmod 0600 "$OUTPUT"
@@ -92,7 +92,7 @@ if [ -n "${STACK_DATABASE_CONNECTION_STRING:-}" ]; then
9292
psql "$STACK_DATABASE_CONNECTION_STRING" -v ON_ERROR_STOP=1 <<SQL
9393
UPDATE "ApiKeySet" SET
9494
"publishableClientKey" = '${STACK_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY}',
95-
"secretServerKey" = '${STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY}',
95+
"secretServerKey" = '${STACK_INTERNAL_PROJECT_SECRET_SERVER_KEY}',
9696
"superSecretAdminKey" = '${STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY}',
9797
"updatedAt" = NOW()
9898
WHERE "projectId" = 'internal' AND id = '3142e763-b230-44b5-8636-aa62f7489c26';

docker/server/entrypoint.sh

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,23 +23,23 @@ fi
2323
# ============= ENV VARS =============
2424

2525
if [ "$NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR" = "true" ]; then
26-
for v in STACK_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY; do
26+
for v in STACK_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY STACK_INTERNAL_PROJECT_SECRET_SERVER_KEY STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY; do
2727
if [ -z "${!v:-}" ]; then
2828
echo "$v must be set in local-emulator mode (injected by the QEMU VM)." >&2
2929
exit 1
3030
fi
3131
done
32-
export STACK_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY
32+
export STACK_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY STACK_INTERNAL_PROJECT_SECRET_SERVER_KEY STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY
3333
else
3434
export STACK_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY=${STACK_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY:-$(openssl rand -base64 32)}
35-
export STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY=${STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY:-$(openssl rand -base64 32)}
35+
export STACK_INTERNAL_PROJECT_SECRET_SERVER_KEY=${STACK_INTERNAL_PROJECT_SECRET_SERVER_KEY:-$(openssl rand -base64 32)}
3636
export STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY=${STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY:-$(openssl rand -base64 32)}
3737
fi
3838

3939
export NEXT_PUBLIC_STACK_PROJECT_ID=internal
4040
export NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=${STACK_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY}
41-
if [ -n "${STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY:-}" ]; then
42-
export STACK_SECRET_SERVER_KEY=${STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY}
41+
if [ -n "${STACK_INTERNAL_PROJECT_SECRET_SERVER_KEY:-}" ]; then
42+
export STACK_SECRET_SERVER_KEY=${STACK_INTERNAL_PROJECT_SECRET_SERVER_KEY}
4343
fi
4444
if [ -n "${STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY:-}" ]; then
4545
export STACK_SUPER_SECRET_ADMIN_KEY=${STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY}
@@ -102,7 +102,7 @@ fi
102102
if [ "$NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR" = "true" ] && [ -n "${STACK_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY:-}" ] && [ -n "${STACK_DATABASE_CONNECTION_STRING:-}" ]; then
103103
# Validate the keys are hex-only to defuse any SQL-injection risk (the VM
104104
# generates them via `openssl rand -hex 32`, so this is an assert, not a filter).
105-
for varname in STACK_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY; do
105+
for varname in STACK_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY STACK_INTERNAL_PROJECT_SECRET_SERVER_KEY STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY; do
106106
val="${!varname:-}"
107107
if [ -z "$val" ]; then
108108
echo "ERROR: $varname is not set; refusing to bootstrap internal api key set." >&2
@@ -118,7 +118,7 @@ if [ "$NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR" = "true" ] && [ -n "${STACK_INTERNAL
118118
INSERT INTO "ApiKeySet" ("projectId", id, description, "expiresAt", "createdAt", "updatedAt", "publishableClientKey", "secretServerKey", "superSecretAdminKey")
119119
VALUES ('internal', '3142e763-b230-44b5-8636-aa62f7489c26', 'Internal API key set', '2099-12-31T23:59:59Z', NOW(), NOW(),
120120
'${STACK_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY}',
121-
'${STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY}',
121+
'${STACK_INTERNAL_PROJECT_SECRET_SERVER_KEY}',
122122
'${STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY}')
123123
ON CONFLICT ("projectId", id) DO UPDATE SET
124124
"publishableClientKey" = EXCLUDED."publishableClientKey",

packages/stack-shared/src/interface/admin-metrics.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -152,12 +152,12 @@ export const UserActivityResponseBodySchema = yupObject({
152152
data_points: MetricsDataPointsSchema,
153153
}).defined();
154154

155-
// Sampled "currently live" users keyed by ISO country code. Populated by
156-
// joining a bounded ClickHouse sample (last N hours of `$token-refresh`
157-
// events grouped by country) with the corresponding Prisma profile rows, so
158-
// the overview globe can render real avatars of real users from each
159-
// country. Optional for one release cycle so clients talking to older
160-
// servers don't fail validation on the returned body.
155+
// Recent "currently live" users keyed by ISO country code. Populated by
156+
// joining a bounded ClickHouse selection from the live `$token-refresh` window
157+
// with the corresponding Prisma profile rows, so the overview globe can render
158+
// real avatars of real users from each country. Optional for one release cycle
159+
// so clients talking to older servers don't fail validation on the returned
160+
// body.
161161
export const MetricsActiveUsersByCountrySchema = yupRecord(
162162
yupString().defined(),
163163
yupArray(MetricsRecentUserSchema).defined(),

0 commit comments

Comments
 (0)