diff --git a/apps/landing/messages/en.json b/apps/landing/messages/en.json index cf3b1a77de..316f6a78c2 100644 --- a/apps/landing/messages/en.json +++ b/apps/landing/messages/en.json @@ -515,6 +515,8 @@ "page_panel_eyebrow": "The surface", "page_cap_eyebrow": "What it does", "page_artifact_eyebrow": "In motion", + "page_gallery_eyebrow": "More screens", + "page_gallery_title": "{feature}, screen by screen", "page_story_eyebrow": "The path", "page_outcome_label": "outcome", "page_cross_eyebrow": "Keep going", diff --git a/apps/landing/messages/ja.json b/apps/landing/messages/ja.json index 40678df407..e36452899c 100644 --- a/apps/landing/messages/ja.json +++ b/apps/landing/messages/ja.json @@ -504,6 +504,8 @@ "page_panel_eyebrow": "\u753b\u9762", "page_cap_eyebrow": "\u3067\u304d\u308b\u3053\u3068", "page_artifact_eyebrow": "\u52d5\u304d", + "page_gallery_eyebrow": "\u305d\u306e\u4ed6\u306e\u753b\u9762", + "page_gallery_title": "{feature}\u306e\u753b\u9762", "page_story_eyebrow": "\u7d4c\u8def", "page_outcome_label": "\u7d50\u679c", "page_cross_eyebrow": "\u6b21\u306f", diff --git a/apps/landing/messages/ko.json b/apps/landing/messages/ko.json index 3250c85902..5850f75fdc 100644 --- a/apps/landing/messages/ko.json +++ b/apps/landing/messages/ko.json @@ -504,6 +504,8 @@ "page_panel_eyebrow": "\ud654\uba74", "page_cap_eyebrow": "\ud560 \uc218 \uc788\ub294 \uc77c", "page_artifact_eyebrow": "\uc6c0\uc9c1\uc784", + "page_gallery_eyebrow": "\ub354 \ub9ce\uc740 \ud654\uba74", + "page_gallery_title": "{feature} \ud654\uba74", "page_story_eyebrow": "\uacbd\ub85c", "page_outcome_label": "\uacb0\uacfc", "page_cross_eyebrow": "\ub2e4\uc74c\uc740", diff --git a/apps/landing/public/screenshots/gallery-alert-create.webp b/apps/landing/public/screenshots/gallery-alert-create.webp new file mode 100644 index 0000000000..85c06663c2 Binary files /dev/null and b/apps/landing/public/screenshots/gallery-alert-create.webp differ diff --git a/apps/landing/public/screenshots/gallery-dashboard-templates.webp b/apps/landing/public/screenshots/gallery-dashboard-templates.webp new file mode 100644 index 0000000000..abac892a29 Binary files /dev/null and b/apps/landing/public/screenshots/gallery-dashboard-templates.webp differ diff --git a/apps/landing/public/screenshots/gallery-issue-detail.webp b/apps/landing/public/screenshots/gallery-issue-detail.webp new file mode 100644 index 0000000000..38f096be19 Binary files /dev/null and b/apps/landing/public/screenshots/gallery-issue-detail.webp differ diff --git a/apps/landing/public/screenshots/gallery-logs-errors.webp b/apps/landing/public/screenshots/gallery-logs-errors.webp new file mode 100644 index 0000000000..378e7f166e Binary files /dev/null and b/apps/landing/public/screenshots/gallery-logs-errors.webp differ diff --git a/apps/landing/public/screenshots/gallery-logs-service.webp b/apps/landing/public/screenshots/gallery-logs-service.webp new file mode 100644 index 0000000000..a9fb02e99c Binary files /dev/null and b/apps/landing/public/screenshots/gallery-logs-service.webp differ diff --git a/apps/landing/public/screenshots/gallery-metric-latency.webp b/apps/landing/public/screenshots/gallery-metric-latency.webp new file mode 100644 index 0000000000..b4df4b9961 Binary files /dev/null and b/apps/landing/public/screenshots/gallery-metric-latency.webp differ diff --git a/apps/landing/public/screenshots/gallery-metric-pool.webp b/apps/landing/public/screenshots/gallery-metric-pool.webp new file mode 100644 index 0000000000..cb4faf0a27 Binary files /dev/null and b/apps/landing/public/screenshots/gallery-metric-pool.webp differ diff --git a/apps/landing/public/screenshots/gallery-metrics-list.webp b/apps/landing/public/screenshots/gallery-metrics-list.webp new file mode 100644 index 0000000000..a59e5b4f18 Binary files /dev/null and b/apps/landing/public/screenshots/gallery-metrics-list.webp differ diff --git a/apps/landing/public/screenshots/gallery-service-dependencies.webp b/apps/landing/public/screenshots/gallery-service-dependencies.webp new file mode 100644 index 0000000000..81e6922f6f Binary files /dev/null and b/apps/landing/public/screenshots/gallery-service-dependencies.webp differ diff --git a/apps/landing/public/screenshots/gallery-service-operations.webp b/apps/landing/public/screenshots/gallery-service-operations.webp new file mode 100644 index 0000000000..c25c2d3cac Binary files /dev/null and b/apps/landing/public/screenshots/gallery-service-operations.webp differ diff --git a/apps/landing/public/screenshots/gallery-trace-flow.webp b/apps/landing/public/screenshots/gallery-trace-flow.webp new file mode 100644 index 0000000000..44a1e557b1 Binary files /dev/null and b/apps/landing/public/screenshots/gallery-trace-flow.webp differ diff --git a/apps/landing/public/screenshots/gallery-trace-waterfall.webp b/apps/landing/public/screenshots/gallery-trace-waterfall.webp new file mode 100644 index 0000000000..d9df42308a Binary files /dev/null and b/apps/landing/public/screenshots/gallery-trace-waterfall.webp differ diff --git a/apps/landing/public/screenshots/gallery-traces-list.webp b/apps/landing/public/screenshots/gallery-traces-list.webp new file mode 100644 index 0000000000..07f2334f41 Binary files /dev/null and b/apps/landing/public/screenshots/gallery-traces-list.webp differ diff --git a/apps/landing/public/screenshots/surface-errors.webp b/apps/landing/public/screenshots/surface-errors.webp index 5e97d2b9a6..762c63025a 100644 Binary files a/apps/landing/public/screenshots/surface-errors.webp and b/apps/landing/public/screenshots/surface-errors.webp differ diff --git a/apps/landing/public/screenshots/surface-service-detail.webp b/apps/landing/public/screenshots/surface-service-detail.webp index 8cb71d28df..ec033c9b61 100644 Binary files a/apps/landing/public/screenshots/surface-service-detail.webp and b/apps/landing/public/screenshots/surface-service-detail.webp differ diff --git a/apps/landing/public/screenshots/surface-service-map.webp b/apps/landing/public/screenshots/surface-service-map.webp index d101af268e..bd23be9821 100644 Binary files a/apps/landing/public/screenshots/surface-service-map.webp and b/apps/landing/public/screenshots/surface-service-map.webp differ diff --git a/apps/landing/public/screenshots/surface-traces-peek.webp b/apps/landing/public/screenshots/surface-traces-peek.webp index 1f2a90241d..d4939d8126 100644 Binary files a/apps/landing/public/screenshots/surface-traces-peek.webp and b/apps/landing/public/screenshots/surface-traces-peek.webp differ diff --git a/apps/landing/scripts/screenshots/shots.ts b/apps/landing/scripts/screenshots/shots.ts index 5f2ed69bf1..55e1b507da 100644 --- a/apps/landing/scripts/screenshots/shots.ts +++ b/apps/landing/scripts/screenshots/shots.ts @@ -28,19 +28,112 @@ export const TIMEZONE = "America/New_York" const lastMinutes = (minutes: number) => ({ fromMinutes: minutes, toMinutes: 0 }) +// Failed checkouts: payment-svc never owns the root span, so filter on the entry point. +const FAILED_CHECKOUTS = { spanNames: ["POST /api/checkout"], hasError: true } + +const openPeek = async (page: Page) => { + await page.locator("tr[data-index]").first().click() + await page.waitForURL(/[?&]peek=/) +} + +/** In-app navigation drops `quota_preview`, so follow a link by URL with the bypass kept. */ +const followLink = async (page: Page, href: string | null, drop: ReadonlyArray = []) => { + if (href === null) throw new Error("no link to follow") + const url = new URL(href, page.url()) + for (const key of drop) url.searchParams.delete(key) + url.searchParams.set("quota_preview", "1") + await page.goto(url.toString()) +} + +/** The first row's own trace page, by its link: the row overlay is not clickable as a link. */ +const openTrace = async (page: Page) => { + const href = await page.locator('a[aria-label^="Open trace"]').first().getAttribute("href") + await followLink(page, href, ["peek", "peekT"]) +} + export const SHOTS: ReadonlyArray = [ + // Distributed tracing + { id: "gallery-traces-list", route: "/traces", range: lastMinutes(60) }, { id: "surface-traces-peek", route: "/traces", - // Failed checkouts: payment-svc never owns the root span, so filter on the entry point. - search: { spanNames: ["POST /api/checkout"], hasError: true }, + search: FAILED_CHECKOUTS, + range: lastMinutes(90), + setup: openPeek, + }, + { + id: "gallery-trace-waterfall", + route: "/traces", + search: FAILED_CHECKOUTS, + range: lastMinutes(90), + setup: openTrace, + }, + { + id: "gallery-trace-flow", + route: "/traces", + search: FAILED_CHECKOUTS, range: lastMinutes(90), setup: async (page) => { - await page.locator("tr[data-index]").first().click() - await page.waitForURL(/[?&]peek=/) + await openTrace(page) + await page.getByRole("tab", { name: /flow/i }).first().click() }, }, + + // Logs + { id: "gallery-logs-errors", route: "/logs", search: { severities: ["ERROR"] }, range: lastMinutes(60) }, + // Severity, not a service or text filter: with those set the facet rail reads empty. + { + id: "gallery-logs-service", + route: "/logs", + search: { severities: ["WARN"] }, + range: lastMinutes(30), + }, + + // Metrics and dashboards + { id: "gallery-metrics-list", route: "/metrics", range: lastMinutes(60) }, + { + id: "gallery-metric-latency", + route: "/metrics/http.server.request.duration", + range: lastMinutes(6 * 60), + }, + { + id: "gallery-metric-pool", + route: "/metrics/db.client.connection.pending_requests", + range: lastMinutes(6 * 60), + }, + { id: "gallery-dashboard-templates", route: "/dashboards/templates" }, + + // Service catalog + { + id: "gallery-service-operations", + route: "/services/payment-svc", + search: { tab: "operations" }, + range: lastMinutes(60), + }, { id: "surface-service-map", route: "/service-map", range: lastMinutes(60) }, { id: "surface-service-detail", route: "/services/payment-svc", range: lastMinutes(6 * 60) }, + { + id: "gallery-service-dependencies", + route: "/services/payment-svc", + search: { tab: "dependencies" }, + range: lastMinutes(60), + }, + + // Errors { id: "surface-errors", route: "/errors" }, + { + id: "gallery-issue-detail", + route: "/errors", + setup: async (page) => { + const row = page.locator('a[href*="/errors/issues/"]', { hasText: "ConnectionTimeout" }).first() + await followLink(page, await row.getAttribute("href")) + }, + }, + + // Alerts + { + id: "gallery-alert-create", + route: "/alerts/create", + search: { serviceName: "payment-svc", template: "high_error_rate" }, + }, ] diff --git a/apps/landing/src/__tests__/feature-galleries.test.ts b/apps/landing/src/__tests__/feature-galleries.test.ts new file mode 100644 index 0000000000..6b83e154a4 --- /dev/null +++ b/apps/landing/src/__tests__/feature-galleries.test.ts @@ -0,0 +1,26 @@ +import { existsSync } from "node:fs" +import { join, resolve } from "node:path" +import { describe, expect, it } from "vitest" +import { SHOTS } from "../../scripts/screenshots/shots" +import { features } from "../lib/features" + +/** + * A gallery shot is only real if the capture script knows how to take it and + * the file it writes is checked in. Either half missing renders a broken image. + */ +const screenshotsDir = resolve(__dirname, "../../public/screenshots") +const shotIds = new Set(SHOTS.map((shot) => shot.id)) + +describe("feature galleries", () => { + const gallery = features.flatMap((feature) => + feature.gallery.map((shot) => ({ slug: feature.slug, id: shot.id })), + ) + + it.each(gallery)("$slug: $id is a capture in shots.ts", ({ id }) => { + expect(shotIds.has(id)).toBe(true) + }) + + it.each(gallery)("$slug: $id is checked in", ({ id }) => { + expect(existsSync(join(screenshotsDir, `${id}.webp`))).toBe(true) + }) +}) diff --git a/apps/landing/src/components/page/FeaturePage.astro b/apps/landing/src/components/page/FeaturePage.astro index 29d07f78d4..443e65a5ce 100644 --- a/apps/landing/src/components/page/FeaturePage.astro +++ b/apps/landing/src/components/page/FeaturePage.astro @@ -8,10 +8,11 @@ * 00 PageHero bg-bg feature engraving (page top) * 01 SurfacePanel bg-bg border-t tone flat → hairline * 02 ArtifactSection bg-bg border-t tone flat → hairline - * 03 [extra sections] per section kubernetes only - * 04 CapabilityLedger bg-bg-deep no border tone steps → tone is the rule - * 05 CrossLinks bg-bg-deep border-t tone flat → hairline - * 06 PageCta amber the one peak + * 03 ScreenGallery bg-bg border-t more of the surface + * 04 [extra sections] per section kubernetes only + * 05 CapabilityLedger bg-bg-deep no border tone steps → tone is the rule + * 06 CrossLinks bg-bg-deep border-t tone flat → hairline + * 07 PageCta amber the one peak * * The tonal rule is tighter here than on the homepage because these pages carry * more frames: `.live-frame` is itself `--bg-deep`, so any section holding one @@ -35,6 +36,7 @@ import CapabilityLedger from "./CapabilityLedger.astro"; import CrossLinks from "./CrossLinks.astro"; import PageCta from "./PageCta.astro"; import PageHero from "./PageHero.astro"; +import ScreenGallery from "./ScreenGallery.astro"; import SurfacePanel from "./SurfacePanel.astro"; import { ARTIFACTS, EXTRA_SECTIONS } from "./slots"; @@ -127,6 +129,17 @@ const crossLinks = [ ) } + { + feature.gallery.length > 0 && ( + + ) + } + {extraSections.map((Section) =>
)} +
+

{eyebrow}

+

+ {title} +

+
+ { + shots.map((shot) => ( +
+
+ {shot.route} + {shot.title} +
+ {shot.alt} +
+ )) + } +
+
+
diff --git a/apps/landing/src/lib/features.ts b/apps/landing/src/lib/features.ts index 1bab80b846..fb28530207 100644 --- a/apps/landing/src/lib/features.ts +++ b/apps/landing/src/lib/features.ts @@ -58,6 +58,32 @@ export const features: Feature[] = [ wide: true, }, extraSections: [], + gallery: [ + { + id: "gallery-traces-list", + route: "/traces", + title: "One row per trace", + alt: "The trace list with one row per request, its services, span count, duration and status", + }, + { + id: "surface-traces-peek", + route: "/traces", + title: "Peek without leaving the list", + alt: "A failed checkout opened in the peek sheet beside the trace list", + }, + { + id: "gallery-trace-waterfall", + route: "/traces/:id", + title: "Where the 5 seconds went", + alt: "A failed checkout's waterfall, with the payment insert waiting five seconds for a pool connection", + }, + { + id: "gallery-trace-flow", + route: "/traces/:id", + title: "Services the request touched", + alt: "The same trace reduced to the services it passed through", + }, + ], capTitle: m.feat_tracing_cap_title, capabilities: [ { op: "/traces", title: m.feat_tracing_cap_1_title, body: m.feat_tracing_cap_1_body }, @@ -106,6 +132,7 @@ export const features: Feature[] = [ wide: true, }, extraSections: [], + gallery: [], capTitle: m.feat_sessions_cap_title, capabilities: [ { op: "event stream", title: m.feat_sessions_cap_1_title, body: m.feat_sessions_cap_1_body }, @@ -138,6 +165,7 @@ export const features: Feature[] = [ wide: true, }, extraSections: ["product-events-sources"], + gallery: [], capTitle: m.feat_pe_cap_title, capabilities: [ { op: "$pageview", title: m.feat_pe_cap_1_title, body: m.feat_pe_cap_1_body }, @@ -187,6 +215,20 @@ export const features: Feature[] = [ lede: m.feat_logs_artifact_lede, }, extraSections: [], + gallery: [ + { + id: "gallery-logs-errors", + route: "/logs", + title: "ERROR across every service", + alt: "Log search filtered to ERROR, with the volume chart and the lines that explain each failure", + }, + { + id: "gallery-logs-service", + route: "/logs", + title: "Warnings before the failures", + alt: "WARN lines: payment-svc reporting 20 of 20 pool connections in use, and slow requests upstream", + }, + ], capTitle: m.feat_logs_cap_title, capabilities: [ { op: "/logs?q=", title: m.feat_logs_cap_1_title, body: m.feat_logs_cap_1_body }, @@ -234,6 +276,32 @@ export const features: Feature[] = [ lede: m.feat_metrics_artifact_lede, }, extraSections: [], + gallery: [ + { + id: "gallery-metrics-list", + route: "/metrics", + title: "Every metric, typed", + alt: "The metric explorer listing each metric with its type, service and last seen time", + }, + { + id: "gallery-metric-latency", + route: "/metrics/:name", + title: "Request duration", + alt: "http.server.request.duration charted over six hours, with the jump after the bad deploy", + }, + { + id: "gallery-metric-pool", + route: "/metrics/:name", + title: "Requests waiting on the pool", + alt: "db.client.connection.pending_requests for payment-svc: zero until the deploy, then 30 to 40 requests waiting for a connection", + }, + { + id: "gallery-dashboard-templates", + route: "/dashboards/templates", + title: "Templates your data supports", + alt: "Dashboard templates sorted by which ones the incoming telemetry can fill", + }, + ], capTitle: m.feat_metrics_cap_title, capabilities: [ { op: "time series", title: m.feat_metrics_cap_1_title, body: m.feat_metrics_cap_1_body }, @@ -285,6 +353,32 @@ export const features: Feature[] = [ wide: true, }, extraSections: [], + gallery: [ + { + id: "gallery-service-operations", + route: "/services/:name", + title: "Operations, slowest first", + alt: "payment-svc operations: the payments queries and POST /charges at 10 to 12% errors and a 5 second p95", + }, + { + id: "surface-service-map", + route: "/service-map", + title: "Built from real calls", + alt: "The service map grouped by namespace, with databases, caches and the failing edge in red", + }, + { + id: "surface-service-detail", + route: "/services/:name", + title: "Deploy on the chart", + alt: "payment-svc latency, Apdex and error rate with the 3.5.0 deploy marked", + }, + { + id: "gallery-service-dependencies", + route: "/services/:name", + title: "What it calls", + alt: "payment-svc dependencies: Postgres at 7.8% errors and a 5 second p95, and api.stripe.com", + }, + ], capTitle: m.feat_catalog_cap_title, capabilities: [ { op: "edges", title: m.feat_catalog_cap_1_title, body: m.feat_catalog_cap_1_body }, @@ -331,6 +425,20 @@ export const features: Feature[] = [ // live artifact would be animation for its own sake. artifact: null, extraSections: [], + gallery: [ + { + id: "gallery-issue-detail", + route: "/errors/issues/:id", + title: "One issue, every occurrence", + alt: "The ConnectionTimeout issue open with its stack trace, trend and affected traces", + }, + { + id: "surface-traces-peek", + route: "/traces", + title: "The trace behind it", + alt: "A failing checkout opened from the issue, showing the span that threw", + }, + ], capTitle: m.feat_errors_cap_title, capabilities: [ { op: "fingerprint", title: m.feat_errors_cap_1_title, body: m.feat_errors_cap_1_body }, @@ -380,6 +488,20 @@ export const features: Feature[] = [ wide: true, }, extraSections: [], + gallery: [ + { + id: "gallery-alert-create", + route: "/alerts/create", + title: "Preview before you save", + alt: "A high error rate rule on payment-svc, previewed against the last day of data", + }, + { + id: "surface-service-detail", + route: "/services/:name", + title: "What it would have caught", + alt: "payment-svc error rate climbing after the 3.5.0 deploy", + }, + ], capTitle: m.feat_alerts_cap_title, capabilities: [ { op: "query builder", title: m.feat_alerts_cap_1_title, body: m.feat_alerts_cap_1_body }, @@ -429,6 +551,7 @@ export const features: Feature[] = [ wide: true, }, extraSections: [], + gallery: [], capTitle: m.feat_mcp_cap_title, capabilities: [ { op: "diagnose_service()", title: m.feat_mcp_cap_1_title, body: m.feat_mcp_cap_1_body }, @@ -462,6 +585,7 @@ export const features: Feature[] = [ wide: true, }, extraSections: ["k8s-console", "k8s-views", "k8s-correlation", "k8s-install"], + gallery: [], capTitle: m.feat_k8s_cap_title, capabilities: [ { op: "helm install", title: m.page_k8s_helm, body: m.page_k8s_helm_desc }, diff --git a/apps/landing/src/lib/page-registry.ts b/apps/landing/src/lib/page-registry.ts index e791f25615..446dd676dd 100644 --- a/apps/landing/src/lib/page-registry.ts +++ b/apps/landing/src/lib/page-registry.ts @@ -85,6 +85,24 @@ interface SurfacePanelSpec { lede: Msg } +/** + * One capture in a feature page's gallery, produced by + * `scripts/screenshots/capture.ts` as `/screenshots/.webp` at + * `GALLERY_SHOT_SIZE`. Literal English, like `Plate`. + */ +export interface GalleryShot { + /** Shot id in `scripts/screenshots/shots.ts`. */ + id: string + /** Frame head, left: the route the shot shows. */ + route: string + /** Frame head, right: what to look at. */ + title: string + alt: string +} + +/** Every capture is 1440×810 CSS px at 2x. */ +export const GALLERY_SHOT_SIZE = { width: 2880, height: 1620 } as const + interface ArtifactSpec { id: ArtifactId title: Msg @@ -107,6 +125,8 @@ export interface Feature { /** One per page. Two live artifacts plus the amber CTA reads speckled. */ artifact: ArtifactSpec | null extraSections: SectionId[] + /** More of the surface, below the artifact. Empty when the demo world cannot show it yet. */ + gallery: GalleryShot[] capTitle: Msg capabilities: Capability[] /** Hand-picked, so no slug ends up with zero inbound links. */ diff --git a/scripts/seed-demo.ts b/scripts/seed-demo.ts index 688d8b3ef4..51819fbab8 100644 --- a/scripts/seed-demo.ts +++ b/scripts/seed-demo.ts @@ -109,6 +109,97 @@ const checkWarehouse = Effect.fn("seedDemo.checkWarehouse")(function* () { } }) +// ── reset ─────────────────────────────────────────────────────────────────── + +const ERROR_STATE_TABLES = [ + "error_fingerprint_candidates", + "error_incidents", + "error_issue_events", + "error_issue_pull_requests", + "error_issue_states", + "error_issue_verifications", + "error_issues", + "error_notification_deliveries", + "investigations", +] + +const Datasources = Schema.Struct({ datasources: Schema.Array(Schema.Struct({ name: Schema.String })) }) + +/** + * `--reset`: start the demo world over, so a re-seed never doubles rows. Only + * ever touches a localhost warehouse and the `maple_screenshots` database. The + * error tick's watermark is set just before the incident: on first sight it + * would otherwise bootstrap to "now" and skip the seeded history. + */ +const resetDemo = Effect.fn("seedDemo.reset")(function* (orgId: string, incidentAt: number) { + const host = env("TINYBIRD_HOST") + const token = env("TINYBIRD_TOKEN") + const pgUrl = env("MAPLE_PG_URL") + if (Option.isNone(host) || Option.isNone(token) || Option.isNone(pgUrl)) { + return yield* new SeedPreflightError({ + message: "--reset needs TINYBIRD_HOST, TINYBIRD_TOKEN and MAPLE_PG_URL", + }) + } + if (!/^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?\/?$/.test(host.value)) { + return yield* new SeedPreflightError({ + message: `--reset only truncates a local warehouse, not ${host.value}`, + }) + } + if ( + !/^postgres(ql)?:\/\/([^@/]*@)?(localhost|127\.0\.0\.1)(:\d+)?\/maple_screenshots(\?|$)/.test( + pgUrl.value, + ) || + // libpq lets these query params override the URL's host, so they would bypass the check above. + /[?&](host|hostaddr|service)=/i.test(pgUrl.value) || + !/^org_[A-Za-z0-9]+$/.test(orgId) + ) { + return yield* new SeedPreflightError({ + message: "--reset only runs against a local maple_screenshots database (bun run seed:demo:env)", + }) + } + + const client = (yield* HttpClient.HttpClient).pipe( + HttpClient.mapRequest(HttpClientRequest.bearerToken(token.value)), + HttpClient.filterStatusOk, + ) + const fail = (cause: { readonly message: string }) => + new SeedPreflightError({ message: `--reset: ${cause.message}` }) + const { datasources } = yield* client.get(`${host.value}/v0/datasources`).pipe( + Effect.flatMap((response) => response.json), + Effect.flatMap(Schema.decodeUnknownEffect(Datasources)), + Effect.mapError(fail), + ) + yield* Effect.forEach( + datasources, + ({ name }) => + client.post(`${host.value}/v0/datasources/${name}/truncate`).pipe(Effect.mapError(fail)), + { concurrency: 4, discard: true }, + ) + + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner + const watermark = new Date(incidentAt - 5 * MINUTE).toISOString() + yield* spawner + .string( + ChildProcess.make("psql", [ + pgUrl.value, + // One transaction: a failed watermark upsert must not leave the error state truncated. + "--single-transaction", + "-v", + "ON_ERROR_STOP=1", + "-c", + `TRUNCATE ${ERROR_STATE_TABLES.join(", ")} CASCADE`, + "-c", + `INSERT INTO error_tick_states (org_id, processed_through, bootstrap_completed, updated_at) + VALUES ('${orgId}', '${watermark}', true, now()) + ON CONFLICT (org_id) DO UPDATE SET processed_through = EXCLUDED.processed_through, + bootstrap_completed = true, claim_token = NULL, claim_expires_at = NULL, updated_at = now()`, + ]), + { includeStderr: true }, + ) + .pipe(Effect.mapError(fail)) + yield* Console.log(` reset: ${datasources.length} datasources truncated, error state cleared`) +}) + // ── sending ───────────────────────────────────────────────────────────────── const post = Effect.fn("seedDemo.post")( @@ -196,6 +287,12 @@ const seed = Command.make( ), Flag.withDefault(false), ), + reset: Flag.Boolean("reset").pipe( + Flag.withDescription( + "Truncate the local warehouse and the demo error state first (screenshot stack only)", + ), + Flag.withDefault(false), + ), resume: Flag.Boolean("resume").pipe( Flag.withDescription("Skip the backfill and follow on from where the last run stopped sending"), Flag.withDefault(false), @@ -270,6 +367,12 @@ const seed = Command.make( peakTracesPerMinute: flags.rate, seed, }) + if (flags.reset && !flags.dryRun && Option.isNone(previous)) { + if (Option.isNone(orgId)) { + return yield* new SeedPreflightError({ message: "--reset needs MAPLE_ORG_ID_OVERRIDE" }) + } + yield* resetDemo(orgId.value, world.incidentAt) + } yield* Console.log( `\nseeding ${clock(world.start)} → ${clock(anchor)} (${hours}h), incident at ${clock(world.incidentAt)}` + (flags.dryRun ? " [dry run]" : ""), diff --git a/scripts/seed-demo/make-env.ts b/scripts/seed-demo/make-env.ts index f0ec8dcafa..040846edec 100644 --- a/scripts/seed-demo/make-env.ts +++ b/scripts/seed-demo/make-env.ts @@ -29,6 +29,9 @@ const OVERRIDES = new Map([ ["INGEST_KEY_STORE_BACKEND", "static"], // Billing off: the plan gate fails open and no test customer lands in the Autumn sandbox. ["AUTUMN_SECRET_KEY", ""], + // The web app's own browser telemetry would otherwise land in the demo org. + ["MAPLE_OTEL_PUBLIC_INGEST_KEY", ""], + ["VITE_MAPLE_INGEST_KEY", ""], ]) /** Point the URL at the screenshot database, also when it names none or ends in a slash. */ diff --git a/scripts/seed-demo/otlp.ts b/scripts/seed-demo/otlp.ts index a681211ff9..2e4854211b 100644 --- a/scripts/seed-demo/otlp.ts +++ b/scripts/seed-demo/otlp.ts @@ -67,8 +67,11 @@ export interface NumberPoint { readonly startTimeUnixNano?: string readonly timeUnixNano: string readonly attributes: ReadonlyArray - readonly asDouble?: number - readonly asInt?: number + /** + * Integer points go out as asDouble too: the gateway's OTLP/JSON shim currently + * stores asInt as 0 (it stringifies a field the proto derive wants as a number). + */ + readonly asDouble: number } export interface HistogramPoint { diff --git a/scripts/seed-demo/telemetry.ts b/scripts/seed-demo/telemetry.ts index 5e7e39205d..5fe33c54aa 100644 --- a/scripts/seed-demo/telemetry.ts +++ b/scripts/seed-demo/telemetry.ts @@ -809,7 +809,7 @@ const buildMetrics = ( memory.push({ timeUnixNano: time, attributes: [], - asInt: Math.round((baseMb + load * 0.8 + rng.next() * 12) * 1_048_576), + asDouble: Math.round((baseMb + load * 0.8 + rng.next() * 12) * 1_048_576), }) } if (cpu.length === 0) continue @@ -827,20 +827,24 @@ const buildMetrics = ( const exhausted = resource.version === INCIDENT.version const used = exhausted ? INCIDENT.poolSize - : Math.min(INCIDENT.poolSize - 4, 2 + Math.round(load * 0.35 + rng.next() * 2)) + : Math.min(8, 2 + Math.round(load * 0.08 + rng.next() * 2)) const time = nano(minute + MINUTE) const pool = attr("db.client.connection.pool.name", "payments") points.push({ timeUnixNano: time, attributes: [pool, attr("db.client.connection.state", "used")], - asInt: used, + asDouble: used, }) points.push({ timeUnixNano: time, attributes: [pool, attr("db.client.connection.state", "idle")], - asInt: INCIDENT.poolSize - used, + asDouble: INCIDENT.poolSize - used, + }) + pending.push({ + timeUnixNano: time, + attributes: [pool], + asDouble: exhausted ? rng.int(24, 48) : 0, }) - pending.push({ timeUnixNano: time, attributes: [pool], asInt: exhausted ? rng.int(24, 48) : 0 }) } if (points.length === 0) continue add(resource, gauge("db.client.connection.count", "{connection}", points)) @@ -855,7 +859,7 @@ const buildMetrics = ( startTimeUnixNano: nano(agg.minute), timeUnixNano: nano(agg.minute + MINUTE), attributes: [], - asInt: agg.count, + asDouble: agg.count, }) orders.set(agg.resource.key, entry) }