@agent-native/core-corpus 0.221.0 → 0.222.0-nightly-20261028092719
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/.agents/skills/first-touch-radar/SKILL.md +19 -14
- package/corpus/templates/analytics/.agents/skills/metric-watch/SKILL.md +5 -2
- package/corpus/templates/analytics/.agents/skills/onboarding-journeys/SKILL.md +22 -4
- package/corpus/templates/analytics/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
- package/corpus/templates/analytics/actions/get-onboarding-journey.ts +3 -2
- package/corpus/templates/analytics/actions/open-traffic-dashboard.ts +1 -0
- package/corpus/templates/analytics/changelog/2026-10-22-outside-assistants-can-open-the-traffic-dashboard-inside-the.md +6 -0
- package/corpus/templates/analytics/package.json +4 -0
- package/corpus/templates/analytics/scripts/first-touch-radar-live.ts +110 -0
- package/corpus/templates/analytics/scripts/journey-snapshot-plan.ts +148 -4
- package/corpus/templates/analytics/scripts/journey-snapshot.ts +308 -120
- package/corpus/templates/analytics/scripts/radar/ab.ts +10 -0
- package/corpus/templates/analytics/scripts/radar/backtest.ts +399 -0
- package/corpus/templates/analytics/scripts/radar/campaign.ts +53 -10
- package/corpus/templates/analytics/scripts/radar/checks.ts +10 -3
- package/corpus/templates/analytics/scripts/radar/live.ts +17 -0
- package/corpus/templates/analytics/scripts/radar/query.ts +312 -83
- package/corpus/templates/analytics/scripts/radar/report.ts +199 -19
- package/corpus/templates/analytics/scripts/radar/sql.ts +89 -18
- package/corpus/templates/analytics/scripts/radar/warehouse-sql.ts +322 -21
- package/corpus/templates/analytics/scripts/radar/watch.ts +203 -129
- package/corpus/templates/analytics/server/lib/onboarding-journey.ts +10 -2
- package/corpus/templates/assets/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
- package/corpus/templates/brain/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
- package/corpus/templates/calendar/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
- package/corpus/templates/calendar/AGENTS.md +1 -0
- package/corpus/templates/calendar/actions/manage-event-draft.ts +33 -11
- package/corpus/templates/calendar/changelog/2026-10-22-outside-assistants-can-open-invite-drafts-in-calendar-for-re.md +6 -0
- package/corpus/templates/chat/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
- package/corpus/templates/clips/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
- package/corpus/templates/content/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
- package/corpus/templates/content/AGENTS.md +1 -0
- package/corpus/templates/content/actions/_preview-document-draft-settlement.ts +31 -0
- package/corpus/templates/content/actions/_preview-document-draft.ts +1 -0
- package/corpus/templates/content/actions/resolve-preview-document-draft.ts +267 -200
- package/corpus/templates/content/actions/share-local-file-document.ts +9 -1
- package/corpus/templates/content/actions/update-preview-document-draft.ts +7 -0
- package/corpus/templates/content/app/components/editor/CommentAiRecipient.tsx +16 -4
- package/corpus/templates/content/app/components/editor/CommentComposer.tsx +1 -1
- package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +71 -209
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +358 -24
- package/corpus/templates/content/app/components/editor/PageDraftRecovery.tsx +43 -6
- package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +52 -14
- package/corpus/templates/content/app/components/editor/SuggestionControls.tsx +191 -0
- package/corpus/templates/content/app/components/editor/SuggestionReviewCard.tsx +1 -0
- package/corpus/templates/content/app/components/editor/ThreadSuggestion.tsx +80 -91
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +4 -0
- package/corpus/templates/content/app/components/editor/comment-ai.tsx +23 -5
- package/corpus/templates/content/app/global.css +7 -0
- package/corpus/templates/content/app/hooks/use-documents.ts +52 -5
- package/corpus/templates/content/app/i18n-data.ts +6 -0
- package/corpus/templates/content/app/lib/document-content-hash.ts +16 -0
- package/corpus/templates/content/changelog/2026-10-17-choosing-use-saved-after-an-interrupted-recovery-no-longer-l.md +6 -0
- package/corpus/templates/content/changelog/2026-10-17-renaming-a-page-while-an-agent-edits-it-no-longer-stops-your.md +6 -0
- package/corpus/templates/content/changelog/2026-10-22-outside-assistants-can-open-shareable-local-file-copies-in-c.md +6 -0
- package/corpus/templates/content/changelog/2026-10-22-pages-using-the-live-copy-keep-unsent-edits-through-a-refres.md +6 -0
- package/corpus/templates/content/changelog/2026-10-26-a-comment-sent-to-ai-says-what-it-asks-for-on-its-send-butto.md +6 -0
- package/corpus/templates/content/changelog/2026-10-26-opening-another-comment-thread-while-you-re-writing-a-new-co.md +6 -0
- package/corpus/templates/content/changelog/2026-10-26-when-the-ai-answers-in-a-long-comment-thread-the-card-shows-.md +6 -0
- package/corpus/templates/content/changelog/2026-10-27-an-ai-suggestion-in-a-comment-thread-uses-the-same-group-as.md +6 -0
- package/corpus/templates/content/changelog/2026-10-27-restoring-page-history-preserves-text-without-exposing-unaut.md +6 -0
- package/corpus/templates/content/server/db/schema.ts +1 -0
- package/corpus/templates/content/server/lib/private-icon-references.ts +37 -14
- package/corpus/templates/content/server/plugins/db.ts +5 -0
- package/corpus/templates/content/shared/api.ts +5 -0
- package/corpus/templates/crm/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
- package/corpus/templates/design/.agents/skills/journey-storyboards/SKILL.md +8 -5
- package/corpus/templates/design/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
- package/corpus/templates/design/.generated/bridge/native-builtin-metadata.generated.ts +326 -0
- package/corpus/templates/design/.generated/bridge/native-worker-source.generated.ts +6 -0
- package/corpus/templates/design/AGENTS.md +3 -1
- package/corpus/templates/design/actions/create-journey-canvas.ts +1 -1
- package/corpus/templates/design/actions/export-coding-handoff.ts +8 -1
- package/corpus/templates/design/actions/generate-screens.ts +6 -0
- package/corpus/templates/design/changelog/2026-10-22-outside-assistants-can-generate-screens-and-open-coding-hand.md +6 -0
- package/corpus/templates/design/server/lib/builder-identity.ts +203 -29
- package/corpus/templates/design/server/plugins/agent-chat.ts +3 -1
- package/corpus/templates/design/server/plugins/auth.ts +9 -4
- package/corpus/templates/design/server/routes/api/builder-identity/exchange.post.ts +1 -1
- package/corpus/templates/design/shared/journey-canvas-messages.ts +1 -2
- package/corpus/templates/design/shared/journey-canvas.ts +143 -55
- package/corpus/templates/dispatch/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
- package/corpus/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +41 -18
- package/corpus/templates/forms/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
- package/corpus/templates/mail/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
- package/corpus/templates/plan/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
- package/corpus/templates/slides/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
- package/corpus/templates/slides/AGENTS.md +6 -0
- package/corpus/templates/slides/actions/clone-context-slide.ts +13 -0
- package/corpus/templates/slides/changelog/2026-10-22-outside-assistants-can-clone-a-slide-from-existing-context-a.md +6 -0
- package/corpus/templates/slides/server/plugins/agent-chat.ts +1 -0
- package/corpus/templates/slides-2/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
- package/corpus/templates/slides-2/AGENTS.md +6 -0
- package/corpus/templates/slides-2/actions/clone-context-slide.ts +6 -0
- package/corpus/templates/slides-2/changelog/2026-10-22-outside-assistants-can-clone-a-slide-from-existing-context-a.md +6 -0
- package/corpus/templates/slides-2/server/plugins/agent-chat.ts +1 -0
- package/package.json +1 -1
package/corpus/README.md
CHANGED
|
@@ -10,8 +10,10 @@ scope: dev
|
|
|
10
10
|
|
|
11
11
|
1. Run `pnpm --filter analytics radar:watch --window daily` and read both the
|
|
12
12
|
JSON and Markdown under `.tmp/first-touch-radar/runs/`.
|
|
13
|
-
2. Treat exit 0 as clear, exit 1 as
|
|
14
|
-
|
|
13
|
+
2. Treat exit 0 as clear, exit 1 as a fired, unavailable, or insufficient
|
|
14
|
+
check needing attention, and exit 2 as a run that could not complete. Keep
|
|
15
|
+
unavailable and insufficient metrics explicit; never turn missing evidence
|
|
16
|
+
into zero or a pass.
|
|
15
17
|
3. On a fire, run `pnpm --filter analytics radar:triage` and inspect the
|
|
16
18
|
outcome-ranked sessions. Use `metric-watch` and `replay-triage` for details.
|
|
17
19
|
4. For a weekly review, run `pnpm --filter analytics radar:report --week
|
|
@@ -24,18 +26,21 @@ scope: dev
|
|
|
24
26
|
report-wide cap, and treat a reached cap as incomplete evidence.
|
|
25
27
|
5. Run `feedback-ingest` for Slack themes. Searches are read-only and begin at
|
|
26
28
|
the saved watermark.
|
|
27
|
-
6. Send one Telegram to Steve only when a check fired
|
|
28
|
-
ready. Include counts and the private
|
|
29
|
-
day. Never send Slack or email.
|
|
30
|
-
7. Keep recipient emails and prompt text out of logs, reports,
|
|
31
|
-
commits. The campaign check reads its mode-0600 recipient file
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
29
|
+
6. Send one Telegram to Steve only when a check fired, is unavailable or is
|
|
30
|
+
insufficient, or the weekly report is ready. Include counts and the private
|
|
31
|
+
run-file path. Send nothing on a quiet day. Never send Slack or email.
|
|
32
|
+
7. Keep recipient emails, HMAC keys, and prompt text out of logs, reports,
|
|
33
|
+
Telegram, and commits. The campaign check reads its mode-0600 recipient file
|
|
34
|
+
in memory, binds keyed email digests as query parameters, disables local and
|
|
35
|
+
BigQuery result caching, and emits aggregate counts only. BigQuery matching
|
|
36
|
+
requires `ANALYTICS_RADAR_CAMPAIGN_HMAC_KEY` and a configured
|
|
37
|
+
`ANALYTICS_RADAR_CAMPAIGN_HMAC_FUNCTION` backed by the same secret server-side.
|
|
38
|
+
The function accepts a normalized email and returns lowercase hex
|
|
39
|
+
`HMAC_SHA256`; keep its key in Secret Manager and never pass the key to
|
|
40
|
+
BigQuery. When either setting is missing, keep the campaign check
|
|
41
|
+
unavailable. Unknown signup campaign labels are grouped as `other`.
|
|
42
|
+
Anonymous landings cannot be assigned to a recipient; do not interpret
|
|
43
|
+
identified counts as a complete click total.
|
|
39
44
|
8. This radar changes scripts and skills only. Do not make user-visible UI
|
|
40
45
|
changes. If UI work becomes necessary, stop that scope and request Steve's
|
|
41
46
|
screenshot approval before implementing it.
|
|
@@ -11,10 +11,13 @@ scope: dev
|
|
|
11
11
|
- Run `pnpm --filter analytics radar:watch --window daily`; use `--window
|
|
12
12
|
hourly` for provisioning and environment-key checks.
|
|
13
13
|
- Read the check status, numerator, denominator, seven-day comparison, and
|
|
14
|
-
coverage. `insufficient` is not a pass
|
|
15
|
-
2
|
|
14
|
+
coverage. `insufficient` is not a pass. `unavailable` and `insufficient`
|
|
15
|
+
remain explicit and make a completed watch exit 1; exit 2 means the run could
|
|
16
|
+
not complete.
|
|
16
17
|
- Compare daily thresholds with the incident window and an ordinary week before
|
|
17
18
|
changing them. Record both windows and the observed counts in the PR or report.
|
|
19
|
+
- Use `pnpm --filter analytics radar:backtest` to compare the Oct 9–23 incident
|
|
20
|
+
window with the Oct 2–8 clean week.
|
|
18
21
|
- Action success uses a weighted `action.response` rate. Keep its app-specific
|
|
19
22
|
threshold below the quiet-week distribution; do not copy the 95% starting
|
|
20
23
|
value without a backtest.
|
|
@@ -18,7 +18,8 @@ The pipeline runs without a UI. Codex does all four steps from the terminal:
|
|
|
18
18
|
2. Run the capture CLI from the agent-native checkout:
|
|
19
19
|
`pnpm --filter analytics journey:capture --tree tree.json --out ./frames`
|
|
20
20
|
(`--per-node 2 --concurrency 3`, `--min-aspect` / `--max-aspect`, `--dry-run`
|
|
21
|
-
to print the plan, `--upload` for private `attachmentRef`s
|
|
21
|
+
to print the plan, `--upload` for private `attachmentRef`s, or
|
|
22
|
+
`--upload --upload-target cdn` for standard Builder CDN URLs).
|
|
22
23
|
3. Call Design's `create-journey-canvas` with
|
|
23
24
|
`{ title, tree, frames: [{ nodeKey, exampleIndex, imageUrl?, attachmentRef?, width, height, capturedAt }], designId? }`
|
|
24
25
|
-> `{ designId, url, nodeCount, frameCount }`. Build `frames` from
|
|
@@ -315,12 +316,14 @@ extraction time. Each snapshot also says whether its frame capture or upload
|
|
|
315
316
|
succeeded and includes the failure reason when it did not. This is visible
|
|
316
317
|
user-role text at the seek, not proof of an exact source event or attempt-ID
|
|
317
318
|
relationship. Unsupported transform, mask, or rounded clipping geometry fails
|
|
318
|
-
the extraction explicitly.
|
|
319
|
-
|
|
319
|
+
the extraction explicitly. The prompt sidecar itself is never uploaded; PNG
|
|
320
|
+
screenshots can use the private attachment action or the explicitly selected
|
|
321
|
+
`cdn.builder.io` upload target.
|
|
320
322
|
|
|
321
323
|
Keep recorded URLs and CSS intact for rrweb playback; network controls belong
|
|
322
324
|
at the capture boundary. `--upload` stores PNGs through the private upload
|
|
323
|
-
action
|
|
325
|
+
action by default; `--upload-target cdn` uses `upload-image` and accepts only
|
|
326
|
+
HTTPS `cdn.builder.io` URLs. Design's own sandboxed template
|
|
324
327
|
previews opt into the cooperative iframe recorder, so their child DOM can be
|
|
325
328
|
replayed without granting the parent access to the preview document or
|
|
326
329
|
credentials.
|
|
@@ -328,3 +331,18 @@ credentials.
|
|
|
328
331
|
Viewport comes from the recording's first rrweb Meta event, stored by replay
|
|
329
332
|
ingest in `session_recordings.metadata.viewport` (`first` and `last`). Older
|
|
330
333
|
recordings have none.
|
|
334
|
+
|
|
335
|
+
## Weekly snapshot CLI
|
|
336
|
+
|
|
337
|
+
Use the cached weekly pipeline when generating or refreshing app storyboards:
|
|
338
|
+
|
|
339
|
+
```bash
|
|
340
|
+
pnpm --filter analytics journey:snapshot --week 2026-10-19 --apps clips,design,slides
|
|
341
|
+
pnpm --filter analytics journey:snapshot --week 2026-10-19 --apps design --experiment first_run_builder_credits
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
`--week` is a closed UTC Monday week. The query starts the previous day to include sessions already underway. Without `--week`, the CLI selects the last full week. It runs the pinned cost preflight and reads one tree per missing app/week/cohort from deployed Analytics; complete trees are cached by app, dates, preset, app URL, and authenticated principal. `--dry-run` prints the request without network access. `--refresh` bypasses existing tree, screenshot, and Design caches and should be used only for an intentional refresh.
|
|
345
|
+
|
|
346
|
+
The output lives under `.tmp/onboarding-journey-snapshots/runs/<week>/`: each app with captured session frames has a Design canvas, and each app has a per-app tree cache and screenshot manifests; the shared `summary.md` includes the cohort trees. Each canvas has a 36 screenshot-backed frame ceiling allocated across its cohort bands. Multi-band canvases place independent trees side by side and keep each band's own root denominator. Same-app cohort trees use `app::cohort::step` node keys so the source app and band denominator stay distinct. Canvas cache keys include the experiment and cohort layout so different runs cannot overwrite one another. The optional experiment accepts `first_run_builder_credits` and `first_run_builder_credits_v2`; control and credits share one canvas, and `summary.md` reports per-arm forks and matching-step week-over-week changes from the full Analytics trees, including cohorts with zero screenshot frames. The canvas contains only screenshot-backed cohorts. Clips first output is `recording_ready`.
|
|
347
|
+
|
|
348
|
+
Design cohorts split web, `/mcp/oauth`, and unknown signup paths. Sessionless MCP OAuth signup events appear as a separate event-grain cohort in `summary.md`, with a week-over-week event-count delta; they have no web replay and must not be represented as screenshot steps. Captured PNGs use `journey:capture --upload-target cdn`; the CLI verifies each result is an HTTPS `cdn.builder.io` URL before passing it to Design. Capture failures remain explicit in the run summary, and the pipeline does not create screenshotless cards.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// AUTO-GENERATED by @agent-native/core deploy build
|
|
2
|
+
import { markDefaultPluginProvided } from "@agent-native/core/server/edge";
|
|
3
|
+
|
|
4
|
+
const pluginStems = ["agent-chat","analytics-alert-jobs","analytics-rollup-backfill-jobs","auth","collab","config","content-metrics-collection-jobs","core-routes","creative-context","dashboard-report-jobs","db","feature-flags","labs","onboarding","otel","session-replay-retention-jobs","transactional-emails","uptime-monitor-jobs"];
|
|
5
|
+
|
|
6
|
+
export default function markBuildDiscoveredPlugins(nitroApp) {
|
|
7
|
+
for (const stem of pluginStems) {
|
|
8
|
+
markDefaultPluginProvided(nitroApp, stem);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
DEFAULT_ONBOARDING_EVENT_ROW_BUDGET,
|
|
15
15
|
getOnboardingJourney,
|
|
16
16
|
MAX_ONBOARDING_EVENT_ROW_BUDGET,
|
|
17
|
+
ONBOARDING_JOURNEY_EXPERIMENT_IDS,
|
|
17
18
|
ONBOARDING_EVENTS_DEFAULT_MAX_TOTAL_BYTES_BILLED,
|
|
18
19
|
ONBOARDING_EVENTS_MAX_BYTES_BILLED,
|
|
19
20
|
ONBOARDING_EVENTS_MAX_TOTAL_BYTES_BILLED,
|
|
@@ -192,10 +193,10 @@ export default defineAction({
|
|
|
192
193
|
"Leave out examples whose viewport was not recorded or cannot be read. Without it they are kept with viewport null and a viewportReason.",
|
|
193
194
|
),
|
|
194
195
|
experimentId: z
|
|
195
|
-
.
|
|
196
|
+
.enum(ONBOARDING_JOURNEY_EXPERIMENT_IDS)
|
|
196
197
|
.optional()
|
|
197
198
|
.describe(
|
|
198
|
-
"
|
|
199
|
+
"Experiment ID to match exactly against each exposure event's experiment_id: first_run_builder_credits or first_run_builder_credits_v2. Returns control and credits session trees from one event read; events for other IDs are not included. Sessions with conflicting or missing assignments remain outside both arms.",
|
|
199
200
|
),
|
|
200
201
|
splitDesignFirstTouch: z
|
|
201
202
|
.boolean()
|
|
@@ -15,6 +15,7 @@ function trafficDashboardDeepLink(): string {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export default defineAction({
|
|
18
|
+
mcpTool: true,
|
|
18
19
|
description:
|
|
19
20
|
"Open the first-party traffic dashboard in the real Analytics app. Use this directly when the user asks to see their traffic dashboard, site traffic, app traffic, or first-party analytics dashboard inline in ChatGPT or Claude. Do not call view-screen, ask_app, or broad resource discovery first for this known dashboard.",
|
|
20
21
|
schema: z.object({}),
|
|
@@ -16,9 +16,13 @@
|
|
|
16
16
|
"journey:capture": "tsx scripts/journey-capture.ts",
|
|
17
17
|
"radar:query": "tsx scripts/radar/query.ts",
|
|
18
18
|
"radar:watch": "tsx scripts/radar/watch.ts",
|
|
19
|
+
"radar:live-watch": "tsx scripts/first-touch-radar-live.ts watch",
|
|
19
20
|
"radar:triage": "tsx scripts/radar/triage.ts",
|
|
20
21
|
"radar:ab": "tsx scripts/radar/ab.ts",
|
|
22
|
+
"radar:live-ab": "tsx scripts/first-touch-radar-live.ts ab",
|
|
21
23
|
"radar:report": "tsx scripts/radar/report.ts",
|
|
24
|
+
"radar:live-report": "tsx scripts/first-touch-radar-live.ts report",
|
|
25
|
+
"radar:backtest": "tsx scripts/first-touch-radar-live.ts backtest",
|
|
22
26
|
"journey:snapshot": "tsx scripts/journey-snapshot.ts",
|
|
23
27
|
"source-index": "tsx scripts/build-source-index.ts",
|
|
24
28
|
"backfill:first-party-bigquery": "tsx scripts/first-party-analytics-bigquery-backfill.ts",
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
runAbReadout,
|
|
7
|
+
formatAbMarkdown,
|
|
8
|
+
parseAbReadoutArgs,
|
|
9
|
+
} from "./radar/ab.ts";
|
|
10
|
+
import {
|
|
11
|
+
formatRadarBacktest,
|
|
12
|
+
parseRadarBacktestArgs,
|
|
13
|
+
runRadarBacktest,
|
|
14
|
+
} from "./radar/backtest.ts";
|
|
15
|
+
import { liveDatabaseUrlFromEnv } from "./radar/live.ts";
|
|
16
|
+
import {
|
|
17
|
+
REPO_ROOT,
|
|
18
|
+
RadarQueryError,
|
|
19
|
+
configureRadarRuntimeEnvironment,
|
|
20
|
+
closeRadarQueries,
|
|
21
|
+
radarRunCliExitCode,
|
|
22
|
+
} from "./radar/query.ts";
|
|
23
|
+
import {
|
|
24
|
+
buildWeeklyReport,
|
|
25
|
+
formatWeeklyMarkdown,
|
|
26
|
+
parseReportArgs,
|
|
27
|
+
writeWeeklyReport,
|
|
28
|
+
} from "./radar/report.ts";
|
|
29
|
+
import { parseWatchArgs, runWatch, writeWatchRun } from "./radar/watch.ts";
|
|
30
|
+
|
|
31
|
+
function loadLiveDatabaseUrl(): void {
|
|
32
|
+
const configuredFile = process.env.ANALYTICS_RADAR_ENV_FILE;
|
|
33
|
+
const envFile = path.resolve(
|
|
34
|
+
REPO_ROOT,
|
|
35
|
+
configuredFile ?? "templates/analytics/.env",
|
|
36
|
+
);
|
|
37
|
+
let contents: string;
|
|
38
|
+
try {
|
|
39
|
+
contents = fs.readFileSync(envFile, "utf8");
|
|
40
|
+
} catch {
|
|
41
|
+
throw new RadarQueryError(
|
|
42
|
+
"Live radar environment file could not be read.",
|
|
43
|
+
2,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
configureRadarRuntimeEnvironment({
|
|
47
|
+
selectedFile: envFile,
|
|
48
|
+
contents,
|
|
49
|
+
databaseUrl: liveDatabaseUrlFromEnv(contents),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function main(): Promise<void> {
|
|
54
|
+
try {
|
|
55
|
+
const [command, ...args] = process.argv.slice(2);
|
|
56
|
+
if (!command || !["watch", "report", "ab", "backtest"].includes(command)) {
|
|
57
|
+
throw new RadarQueryError(
|
|
58
|
+
"Usage: radar:live-watch [watch options], radar:live-report [report options], radar:live-ab [A/B options], or radar:backtest [--max-total-gb N].",
|
|
59
|
+
2,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
loadLiveDatabaseUrl();
|
|
63
|
+
|
|
64
|
+
if (command === "watch") {
|
|
65
|
+
const run = await runWatch(parseWatchArgs(args));
|
|
66
|
+
const paths = await writeWatchRun(run);
|
|
67
|
+
console.log(JSON.stringify({ ...paths, exitCode: run.exitCode }));
|
|
68
|
+
process.exitCode = run.exitCode;
|
|
69
|
+
} else if (command === "report") {
|
|
70
|
+
const { week, skipJourneySnapshot, maxTotalGb, freshBigQuery } =
|
|
71
|
+
parseReportArgs(args);
|
|
72
|
+
if (!week) {
|
|
73
|
+
throw new RadarQueryError(
|
|
74
|
+
"Usage: radar:live-report --week YYYY-MM-DD [--max-total-gb N]",
|
|
75
|
+
2,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
const report = await buildWeeklyReport(week, {
|
|
79
|
+
skipJourneySnapshot,
|
|
80
|
+
maxTotalGb,
|
|
81
|
+
freshBigQuery,
|
|
82
|
+
});
|
|
83
|
+
const paths = await writeWeeklyReport(report);
|
|
84
|
+
console.log(formatWeeklyMarkdown(report));
|
|
85
|
+
console.log(JSON.stringify({ ...paths, exitCode: report.exitCode }));
|
|
86
|
+
process.exitCode = report.exitCode;
|
|
87
|
+
} else if (command === "ab") {
|
|
88
|
+
const result = await runAbReadout(parseAbReadoutArgs(args));
|
|
89
|
+
console.log(formatAbMarkdown(result));
|
|
90
|
+
console.log(JSON.stringify(result));
|
|
91
|
+
} else {
|
|
92
|
+
const result = await runRadarBacktest(parseRadarBacktestArgs(args));
|
|
93
|
+
console.log(formatRadarBacktest(result));
|
|
94
|
+
}
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.error(
|
|
97
|
+
error instanceof Error ? error.message : "Live radar could not run.",
|
|
98
|
+
);
|
|
99
|
+
process.exitCode = radarRunCliExitCode(error);
|
|
100
|
+
} finally {
|
|
101
|
+
await closeRadarQueries();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (
|
|
106
|
+
process.argv[1] &&
|
|
107
|
+
path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
|
|
108
|
+
) {
|
|
109
|
+
await main();
|
|
110
|
+
}
|
|
@@ -14,8 +14,8 @@ export const SNAPSHOT_PRESET = Object.freeze({
|
|
|
14
14
|
maxTotalBytesBilled: 275_000_000_000,
|
|
15
15
|
minWidth: 1100,
|
|
16
16
|
maxWidth: 1920,
|
|
17
|
-
minAspect: 1.
|
|
18
|
-
maxAspect: 1
|
|
17
|
+
minAspect: 1.2,
|
|
18
|
+
maxAspect: 2.1,
|
|
19
19
|
requireKnownViewport: true,
|
|
20
20
|
recency: "newest" as const,
|
|
21
21
|
settleMs: 300,
|
|
@@ -32,6 +32,7 @@ export interface SnapshotExample {
|
|
|
32
32
|
|
|
33
33
|
export interface SnapshotNode {
|
|
34
34
|
key: string;
|
|
35
|
+
cohortKey?: string;
|
|
35
36
|
label: string;
|
|
36
37
|
parentKey: string | null;
|
|
37
38
|
depth: number;
|
|
@@ -47,7 +48,9 @@ export interface SnapshotNode {
|
|
|
47
48
|
export interface SnapshotTree {
|
|
48
49
|
window: { from: string; to: string };
|
|
49
50
|
app: string;
|
|
51
|
+
populationGrain?: "sessions" | "signup_events";
|
|
50
52
|
rootN: number;
|
|
53
|
+
appRootN?: Record<string, number>;
|
|
51
54
|
coverage: {
|
|
52
55
|
sessionsWithEvents: number;
|
|
53
56
|
sessionsWithReplay: number | null;
|
|
@@ -60,12 +63,61 @@ export interface SnapshotTree {
|
|
|
60
63
|
[key: string]: unknown;
|
|
61
64
|
}
|
|
62
65
|
|
|
66
|
+
export function snapshotPopulationUnit(
|
|
67
|
+
tree: Pick<SnapshotTree, "populationGrain">,
|
|
68
|
+
): "sessions" | "signup events" {
|
|
69
|
+
return tree.populationGrain === "signup_events"
|
|
70
|
+
? "signup events"
|
|
71
|
+
: "sessions";
|
|
72
|
+
}
|
|
73
|
+
|
|
63
74
|
export interface SnapshotBand {
|
|
64
75
|
key: string;
|
|
65
76
|
tree: SnapshotTree;
|
|
66
77
|
}
|
|
67
78
|
|
|
68
|
-
|
|
79
|
+
export interface SnapshotSummaryBand extends SnapshotBand {
|
|
80
|
+
label: string;
|
|
81
|
+
previousTree?: SnapshotTree;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function snapshotSummaryBands(
|
|
85
|
+
bands: readonly (SnapshotBand & { label: string })[],
|
|
86
|
+
previousTrees: ReadonlyMap<string, SnapshotTree>,
|
|
87
|
+
): SnapshotSummaryBand[] {
|
|
88
|
+
return bands.map(({ key, label, tree }) => {
|
|
89
|
+
const previousTree = previousTrees.get(key);
|
|
90
|
+
return {
|
|
91
|
+
key,
|
|
92
|
+
label,
|
|
93
|
+
tree,
|
|
94
|
+
...(previousTree ? { previousTree } : {}),
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function snapshotCanvasTitle(input: {
|
|
100
|
+
app: string;
|
|
101
|
+
week: string;
|
|
102
|
+
experimentId?: string;
|
|
103
|
+
singleBand?: Pick<SnapshotSummaryBand, "key" | "label">;
|
|
104
|
+
hasFirstTouchBands: boolean;
|
|
105
|
+
}): string {
|
|
106
|
+
const suffixes = [
|
|
107
|
+
...(input.experimentId ? [`Experiment ${input.experimentId}`] : []),
|
|
108
|
+
...(input.singleBand && input.singleBand.key !== input.app
|
|
109
|
+
? [input.singleBand.label]
|
|
110
|
+
: input.hasFirstTouchBands
|
|
111
|
+
? ["first-touch cohorts"]
|
|
112
|
+
: []),
|
|
113
|
+
];
|
|
114
|
+
return `Onboarding snapshot · ${input.app} · ${input.week}${suffixes.length ? ` · ${suffixes.join(" · ")}` : ""}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function arrayItemAt<T>(
|
|
118
|
+
items: readonly T[],
|
|
119
|
+
index: number,
|
|
120
|
+
): T | undefined {
|
|
69
121
|
return items.slice(index, index + 1)[0];
|
|
70
122
|
}
|
|
71
123
|
|
|
@@ -95,6 +147,12 @@ export function validateSnapshotCostEstimate(
|
|
|
95
147
|
return estimatedBytes as number;
|
|
96
148
|
}
|
|
97
149
|
|
|
150
|
+
export function isBuilderCdnUrl(value: unknown): value is string {
|
|
151
|
+
if (typeof value !== "string" || !URL.canParse(value)) return false;
|
|
152
|
+
const url = new URL(value);
|
|
153
|
+
return url.protocol === "https:" && url.hostname === "cdn.builder.io";
|
|
154
|
+
}
|
|
155
|
+
|
|
98
156
|
export function defaultSnapshotWeek(now = new Date()): string {
|
|
99
157
|
if (!Number.isFinite(now.valueOf())) {
|
|
100
158
|
throw new Error("The current date must be valid.");
|
|
@@ -173,9 +231,21 @@ export function snapshotCanvasCacheKey(input: {
|
|
|
173
231
|
designUrl: string;
|
|
174
232
|
designScopeHash: string;
|
|
175
233
|
analyticsScopeHash: string;
|
|
234
|
+
layout: {
|
|
235
|
+
experimentId: string | null;
|
|
236
|
+
cohortKeys: readonly string[];
|
|
237
|
+
};
|
|
176
238
|
}): string {
|
|
177
239
|
return createHash("sha256")
|
|
178
|
-
.update(
|
|
240
|
+
.update(
|
|
241
|
+
JSON.stringify({
|
|
242
|
+
...input,
|
|
243
|
+
layout: {
|
|
244
|
+
...input.layout,
|
|
245
|
+
cohortKeys: [...input.layout.cohortKeys].sort(),
|
|
246
|
+
},
|
|
247
|
+
}),
|
|
248
|
+
)
|
|
179
249
|
.digest("hex")
|
|
180
250
|
.slice(0, 20);
|
|
181
251
|
}
|
|
@@ -363,6 +433,80 @@ export function selectCaptureTree<T extends SnapshotTree>(
|
|
|
363
433
|
};
|
|
364
434
|
}
|
|
365
435
|
|
|
436
|
+
export function combineSnapshotBands(
|
|
437
|
+
bands: readonly SnapshotBand[],
|
|
438
|
+
sourceApp?: string,
|
|
439
|
+
): SnapshotTree {
|
|
440
|
+
const first = arrayItemAt(bands, 0);
|
|
441
|
+
if (bands.length < 2 || !first) {
|
|
442
|
+
throw new Error(
|
|
443
|
+
"At least two cohorts are required for a side-by-side tree.",
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
if (sourceApp !== undefined && !/^[a-z][a-z0-9-]{0,127}$/.test(sourceApp)) {
|
|
447
|
+
throw new Error(
|
|
448
|
+
`Invalid source app for side-by-side cohorts: ${sourceApp}`,
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
const appRootN: Record<string, number> = {};
|
|
452
|
+
const nodes: SnapshotNode[] = [];
|
|
453
|
+
const nodeKeys = new Set<string>();
|
|
454
|
+
|
|
455
|
+
for (const band of bands) {
|
|
456
|
+
if (!/^[a-z][a-z0-9-]{0,127}$/.test(band.key)) {
|
|
457
|
+
throw new Error(`Invalid side-by-side cohort key: ${band.key}`);
|
|
458
|
+
}
|
|
459
|
+
if (
|
|
460
|
+
band.tree.window.from !== first.tree.window.from ||
|
|
461
|
+
band.tree.window.to !== first.tree.window.to
|
|
462
|
+
) {
|
|
463
|
+
throw new Error("Side-by-side journey cohorts must use the same window.");
|
|
464
|
+
}
|
|
465
|
+
if (!Number.isSafeInteger(band.tree.rootN) || band.tree.rootN < 1) {
|
|
466
|
+
throw new Error(`Cohort ${band.key} needs a positive root denominator.`);
|
|
467
|
+
}
|
|
468
|
+
const rootKey = sourceApp ? `${sourceApp}::${band.key}` : band.key;
|
|
469
|
+
if (Object.prototype.hasOwnProperty.call(appRootN, rootKey)) {
|
|
470
|
+
throw new Error(`Duplicate side-by-side cohort key: ${rootKey}`);
|
|
471
|
+
}
|
|
472
|
+
appRootN[rootKey] = band.tree.rootN;
|
|
473
|
+
const prefix = sourceApp ? `${sourceApp}::${band.key}` : band.key;
|
|
474
|
+
for (const node of band.tree.nodes) {
|
|
475
|
+
const key = `${prefix}::${node.key}`;
|
|
476
|
+
if (key.length > 2048 || nodeKeys.has(key)) {
|
|
477
|
+
throw new Error(`Side-by-side journey node key is invalid: ${key}`);
|
|
478
|
+
}
|
|
479
|
+
nodeKeys.add(key);
|
|
480
|
+
nodes.push({
|
|
481
|
+
...node,
|
|
482
|
+
key,
|
|
483
|
+
...(sourceApp ? { cohortKey: band.key } : {}),
|
|
484
|
+
parentKey: node.parentKey ? `${prefix}::${node.parentKey}` : null,
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const sessionsWithReplay = bands.reduce<number | null>((total, band) => {
|
|
490
|
+
const count = band.tree.coverage.sessionsWithReplay;
|
|
491
|
+
return total === null || count === null ? null : total + count;
|
|
492
|
+
}, 0);
|
|
493
|
+
return {
|
|
494
|
+
window: first.tree.window,
|
|
495
|
+
app: "all",
|
|
496
|
+
rootN: bands.reduce((total, band) => total + band.tree.rootN, 0),
|
|
497
|
+
appRootN,
|
|
498
|
+
coverage: {
|
|
499
|
+
sessionsWithEvents: bands.reduce(
|
|
500
|
+
(total, band) => total + band.tree.coverage.sessionsWithEvents,
|
|
501
|
+
0,
|
|
502
|
+
),
|
|
503
|
+
sessionsWithReplay,
|
|
504
|
+
truncated: bands.some((band) => band.tree.coverage.truncated),
|
|
505
|
+
},
|
|
506
|
+
nodes,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
366
510
|
export function allocateFrameBudgets(
|
|
367
511
|
bands: readonly SnapshotBand[],
|
|
368
512
|
maxFrames = SNAPSHOT_MAX_FRAMES,
|