feat(telemetry): e2e test suite + create-blocks-app worker parity + cross-platform console#133
feat(telemetry): e2e test suite + create-blocks-app worker parity + cross-platform console#133sarayev wants to merge 44 commits into
Conversation
…pp from local dist - AppSetting used a fixed SSM parameter name (/telemetry-e2e/api-url) shared across every uniquely-named stack, so CloudFormation rejected the changeset with 'AWS::SSM::Parameter ... already exists' — failing the sandbox, deploy, and console (downstream) SUCCESS tests. Derive the name from scope.fullId (which embeds the unique stack name), mirroring the comprehensive e2e app. - create-blocks-app SUCCESS invoked 'npm exec create-blocks-app', which resolved the unpublished unscoped name from the public registry (404). Invoke the built local binary (packages/create-blocks-app/dist/index.js) directly instead. - Widen sandbox SUCCESS failure diagnostic truncation 500->2000 chars so a real CDK/CloudFormation error is visible in CI output.
…imeout The pre-cleanup destroy inside sandbox.ts/deploy.ts emitted its own sandbox:destroy / destroy telemetry into the same --telemetry-file the test asserts, so SUCCESS tests read the wrong command, and without creds the destroy retry-backoff blew the suite time budget (sandbox FAIL 9s -> 96s). Stale-stack collisions were the original reason for pre-cleanup, but unique per-stack SSM names now prevent them, so the destroy is unnecessary. Each script emits exactly one event again. Raise the suite timeout to 40min to fit the real sequential CDK deploy/destroy cycles within the 60min job.
openConsole hardcoded macOS 'open', which is absent on Linux CI runners and remote shells, so the console command emitted FAIL (the console SUCCESS e2e test failed). Pick the opener per platform (open/xdg-open/start), resolve the region from AWS_REGION/AWS_DEFAULT_REGION before falling back to aws config, and treat a missing opener as best-effort success — the URL is still printed.
Closes the gap with the private-amplify-backend-staging PR #920 test suite. Added: - Network resilience (3 tests: broken endpoint invisible/visible, no crash) - Schema forward-compatibility (2 tests: JSON-serializable, required fields) - Environment metadata (2 tests: nodeVersion/os/ci, platform match) - --telemetry-file sink (3 tests: writes enabled, writes when HTTP disabled, projectId persists) - Consent notice (1 test: silent on subsequent runs) - blocks-telemetry CLI (7 tests: help, disable, enable, status, global) - Disable edge case (1 test: =true does NOT disable)
…ssues Added: network resilience, schema forward-compat, environment metadata, --telemetry-file sink, consent notice, blocks-telemetry CLI, disable edge case. Fixes: - runCommand no longer force-overrides NODE_DEBUG (allows network invisibility test) - no-crash test uses spawnDevServer (proves dev starts despite broken endpoint) - CLI --disable/--enable tests use try/finally for safe config restoration
npx blocks-telemetry relies on the workspace bin symlink and the exec bit surviving the tsc build step, which is fragile in CI (Permission denied / unresolved bin -> non-zero exit, no usage output). Invoke the built telemetry-cli.js entrypoint directly with node, mirroring the create-blocks-app approach, so the 7 CLI tests deploy deterministically.
… assertion strictness - README: remove references to non-existent emit.ts, seed-telemetry-id action, and cleanup command; describe actual test mechanism - getNextPort(): use net.createServer port 0 for OS-assigned ephemeral port (eliminates random collision failures) - FAIL-path assertions: vendorize and sandbox:destroy now assert.ok(waitForFile) instead of silently skipping inside if() - create-blocks-app FAIL: explicitly assert file is NOT written (documented behavior) - Disable test: assert fileExists for --telemetry-file-writes-when-disabled contract
…of broken async listen The previous fix used net.createServer().listen(0) which is async — .address() returned null before listen completed, breaking all dev server tests. Using a simple sequential counter from a high port range.
…ertion - vendorize: use 'npm exec' instead of 'npx' (same fix as create-blocks-app) - assertDelivered: accept 'spawned telemetry subprocess' as dispatch proof (500ms timeout in CI is a network condition, not a code bug)
… emitted vendorize calls process.exit(1) on unresolvable packages, bypassing trackCommand's finally block. The test now correctly asserts that no telemetry file is written (documenting the known limitation).
…d only The process-group SIGKILL (process.kill(-pid, 'SIGKILL')) was propagating to the test runner after PR #136 added dev server process tree management. Now sends SIGTERM to the child process only, with a fallback SIGKILL.
The dev server's new process-tree management (PR #136) propagates signals back to the process group. Without detached, the child is in the test runner's group and kill signals stay contained.
The dev server (PR #136) uses process-group signals internally for its frontend tree management. Without detached:true, those signals propagate to the test runner. With detached:true, the dev server is in its own process group — killing the group is safe and doesn't affect the runner.
… naturally The new dev server (PR #136) has internal process-tree management that propagates kill signals. Instead of killing the process group, just disconnect stdio and unref — let the process die when the test runner exits.
reclaimPort() discovers the PID holding a port via findListenerPids() (lsof -ti tcp:<port> -sTCP:LISTEN) and group-kills it (kill(-pid, SIGTERM)) to free a stale/orphaned socket before startup. When the port is held by the current process tree — e.g. a test runner that binds the port itself before spawning the dev server as a child — lsof resolves to the runner's own PID (an ancestor of the dev server). The subsequent process-group kill then tears down the runner and its whole group, surfacing as an immediate SIGTERM / exit 143. Guard findListenerPids() against the current process and every ancestor of it (collectSelfAndAncestorPids, walking /proc/<pid>/stat ppid with a ps -o ppid= fallback). A listener that resolves to us or a forebear is the process tree running the dev server, so reclaiming it is never correct — the port-in-use case then falls through to the normal EADDRINUSE path and emits dev/FAIL PORT_IN_USE as intended. Product dev-server behaviour change (hardening), covered by unit tests for the guard, the ancestry walk, and the /proc/ps parent lookup.
Reverts commit 0374957 (packages/core/src/scripts/process-tree.ts and dev-server-reclaim.test.ts) so this PR no longer changes the core dev server. The dev FAIL e2e test is reworked to trigger a failure that does not bind a port inside the test-runner process, so PR #136's reclaim can no longer group-kill the runner.
…listener) Replace the port-in-use dev FAIL test — which bound a port inside the test-runner process and caused PR #136's startup reclaim to group-kill the runner (SIGTERM/exit 143) — with a failure that binds the dev server front door to a privileged port (80). As a non-root user this fails with EACCES on listen, routed through the dev server's server.on('error') handler, which still emits dev/FAIL telemetry. No listener is created in the runner/ancestor, so reclaim is a no-op. Assert only command=dev / state=FAIL (no specific error code/phase). Drop the unused node:net createServer import and the blocker teardown.
| if (config.telemetry?.enabled === true) return true; | ||
| } catch { /* no config */ } | ||
|
|
||
| if (isCI()) return false; |
There was a problem hiding this comment.
Can we distinguish between "human" and CI use in the dashboard?
There was a problem hiding this comment.
Yes, we can. Here, this check was preventing from collecting events in ci for the create-blocks-app command, which I am removing now.
…e-tests-clean # Conflicts: # .github/workflows/pr-checks.yml
🦋 Changeset detectedLatest commit: 47ef470 The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
- cross-reference comments between duplicated telemetry-send-worker copies - optional chaining for child.stdin in telemetry.ts and core client.ts - typed TelemetryPayload interface in e2e test; remove duplicate JSDoc - correct README per-command coverage bullet
|
Thanks for the thorough pass! Addressed all the nits/minor items in 47ef470. Left the send-worker de-duplication as a tracked follow-up (rationale in-thread), and confirmed the isCI() removal is safe on the metrics side (events carry a ci flag; dashboards exclude CI data). Replies inline per comment. |
|
|
||
| /** Assert that the event was delivered to the real endpoint. */ | ||
| function assertDelivered(stderr: string, description = ''): void { | ||
| assert.match(stderr, SENT_REGEX, `Telemetry should be delivered to endpoint. ${description}\nstderr: ${stderr.slice(-500)}`); |
There was a problem hiding this comment.
[major] Delivery tests don't override BLOCKS_TELEMETRY_ENDPOINT, so they hit the production default (https://blocks-telemetry.us-east-1.api.aws/metrics, per packages/core/src/telemetry/client.ts:16-20) using the pinned installation ID 00000000-0000-0000-0000-000000000e2e seeded in the workflow. Every source-changed same-repo PR posts real synthetic events to production telemetry. Confirm the backend excludes that ID from metrics, or route delivery verification to a test endpoint. I'd gate merge on this.
There was a problem hiding this comment.
This is actually the existing repo-wide behavior, not something this suite introduces. Telemetry emits unconditionally in CI (consent.ts: "CI environments do NOT suppress telemetry" — only AWS_BLOCKS_DISABLE_TELEMETRY=1/project/global config disable it), no workflow overrides BLOCKS_TELEMETRY_ENDPOINT anywhere (grep .github/ → 0 hits), and six other e2e jobs (e2e-templates/sandbox/sandbox-vpc/production/supabase/hosting) already seed the same pinned ID …000000000e2e via ./.github/actions/seed-telemetry-id and run the real CLI against the prod endpoint. The backend also flags and excludes CI data (and this pinned ID) from metrics, so there's no metric pollution. So I'd treat changing this as a repo-wide follow-up rather than gating this PR. (The delivery-assertion flakiness angle from your :47 comment is the part genuinely worth tightening — tracking that as a follow-up.)
| run: | | ||
| cd test-apps/telemetry | ||
| npx tsx aws-blocks/scripts/sandbox-destroy.ts || true | ||
| npx tsx aws-blocks/scripts/destroy.ts || true |
There was a problem hiding this comment.
[major] Both destroys end in || true, so a failed teardown is swallowed. Stacks are unique per run (BLOCKS_STACK_SUFFIX: telemetry-<pr>-<attempt> + a random sandbox id), so leaks are distinct and cumulative -> ongoing AWS cost. Fail the job (or emit a warning annotation) on cleanup error, and/or add a scheduled sweeper for stale bb-telemetry-e2e-* stacks (or tag blocks:purpose=telemetry-e2e).
There was a problem hiding this comment.
The best-effort teardown here mirrors the existing convention: e2e-hosting's cleanup step uses the identical if: always() + || true pattern (line 475), and the other e2e jobs rely on in-script cleanup + the separate cleanup-stacks.yml sweeper. Stacks are uniquely named per run, so a genuinely stronger story (fail-on-cleanup-error and/or a scheduled sweeper for bb-telemetry-e2e-*) is a reasonable improvement — but to stay consistent with the repo's current approach I'd do that as a follow-up (ideally across all e2e jobs, not just this one). Tracking it.
| } catch { /* no config */ } | ||
|
|
||
| if (isCI()) return false; | ||
| return true; |
There was a problem hiding this comment.
[minor] Removing if (isCI()) return false; here is a user-visible change: consumers running create-blocks-app in their own CI now emit telemetry that was previously suppressed. Intentional and consistent with core (consent.ts documents "CI environments do NOT suppress telemetry"), but please surface it in the user-facing consent docs and release notes, not just the changeset. (No dead code: isCI() is still used as the ci: event attribute.)
There was a problem hiding this comment.
Our telemetry docs already clearly state that we collect telemetry and that collection is not disabled in CI environments — that's the documented, intended behavior. The previous isCI() suppression in create-blocks-app was inconsistent with both that documentation and the core package (consent.ts), so removing it is a bug fix that brings create-blocks-app in line with the documented behavior. It doesn't introduce a new user-visible policy, so no special customer-facing wording or release-note callout is needed beyond the changeset. (Confirmed no dead code — isCI() still feeds the ci: event attribute.)
| import { request as httpsRequest } from 'node:https'; | ||
| import { request as httpRequest } from 'node:http'; | ||
|
|
||
| // Matches the timeout in packages/core/src/telemetry/telemetry-send-worker.ts — keep in sync. |
There was a problem hiding this comment.
[minor] This worker is byte-identical to packages/core/src/telemetry/telemetry-send-worker.ts except this header/comment, so manual "keep in sync" will drift. Since they're already identical modulo the header, add a CI check asserting byte-equality (ignoring the header comment), or generate one from the other at build time so core is the single source of truth.
There was a problem hiding this comment.
Good point — the two workers are identical modulo the header comment, so manual "keep in sync" will drift. Adding a CI byte-equality check (ignoring the header) or generating one from the other at build time (core as SSOT) is the right call. This is part of the send-worker de-duplication follow-up we're already tracking; I'll fold the drift guard into it.
| let payload = ''; | ||
| process.stdin.setEncoding('utf-8'); | ||
| process.stdin.on('data', (chunk: string) => { payload += chunk; }); | ||
| process.stdin.on('end', () => { |
There was a problem hiding this comment.
[minor] The request is fired only inside the end handler, and timeout: TIMEOUT_MS is the HTTP request timeout, not a stdin watchdog -- if end never fires the process has no self-kill. Low risk (detached/unref'd, tiny payload, parent exit normally closes the pipe), but a top-level setTimeout(() => process.exit(1), TIMEOUT_MS * 2).unref() is cheap insurance.
There was a problem hiding this comment.
Fair — timeout: TIMEOUT_MS is the HTTP request timeout, not a stdin watchdog, so if end never fires there's no self-kill. Risk is low (detached/unref'd, tiny payload, parent exit closes the pipe), but a top-level setTimeout(() => process.exit(1), TIMEOUT_MS * 2).unref() is cheap insurance. Adding it to the follow-up.
| const child = spawn(process.execPath, [workerPath, endpoint], { | ||
| detached: true, | ||
| stdio: ['pipe', 'ignore', 'ignore'], | ||
| stdio: ['pipe', 'ignore', process.env.NODE_DEBUG?.includes('blocks-telemetry') ? 'inherit' : 'ignore'], |
There was a problem hiding this comment.
[nit] process.env.NODE_DEBUG?.includes('blocks-telemetry') is a looser substring match than the debuglog('blocks-telemetry') token semantics used for the debug fn (e.g. NODE_DEBUG=blocks-telemetry-foo flips stdio to inherit but wouldn't activate debuglog). The same expression is duplicated in packages/create-blocks-app/src/telemetry.ts. Consider a tiny shared helper for the two non-worker sites (workers stay import-free).
There was a problem hiding this comment.
Noted — NODE_DEBUG?.includes('blocks-telemetry') is looser than debuglog's token semantics (e.g. blocks-telemetry-foo would flip stdio but not activate debuglog), and it's duplicated in create-blocks-app's telemetry. A tiny shared helper for the two non-worker sites makes sense; rolling it into the follow-up.
| process.platform === 'darwin' ? 'open' : | ||
| process.platform === 'win32' ? 'cmd' : | ||
| 'xdg-open'; | ||
| const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url]; |
There was a problem hiding this comment.
[nit] cmd /c start "" <url> (the empty-title arg) is correct today, but if the CloudFormation URL template ever gains &-joined query params, cmd's start will break on them. Add a comment noting the constraint, or switch to PowerShell Start-Process.
There was a problem hiding this comment.
Good defensive catch — cmd /c start "" <url> is fine for today's console URLs, but would break if the URL template ever gains &-joined query params. I'll add a constraint comment (or switch to PowerShell Start-Process) as part of the follow-up.
| const PINNED_INSTALLATION_ID = '00000000-0000-0000-0000-000000000e2e'; | ||
| const PINNED_PROJECT_ID = '00000000-0000-0000-0000-0000000e2e57'; | ||
| const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; | ||
| const SENT_REGEX = /BLOCKS-TELEMETRY: sent \(status=200\)/; |
There was a problem hiding this comment.
[nit] These status=200 assertions couple CI green to the live endpoint's uptime, risking flakiness on unrelated PRs. The infra to fix already exists -- the network-resilience test sets BLOCKS_TELEMETRY_ENDPOINT to a local address, so pointing the per-command matrix at a 127.0.0.1 sink is a small change; keep one real-endpoint smoke test.
There was a problem hiding this comment.
This is the one I most agree is worth tightening: unlike the other e2e jobs (which fire-and-forget telemetry), our suite asserts delivery status=200, which couples CI green to the live endpoint's uptime and can flake on unrelated PRs. The infra already exists — the network-resilience test points BLOCKS_TELEMETRY_ENDPOINT at a local sink, so I'll route the per-command delivery matrix at a 127.0.0.1 sink and keep a single real-endpoint smoke test. Tracking as a follow-up.
|
Thanks for the detailed review! Dispositions: the two majors (#1 prod endpoint, #2 |
What
Comprehensive end-to-end telemetry test suite that verifies real event delivery across all CLI commands, plus production fixes discovered during testing.
Production Changes
NODE_DEBUG=blocks-telemetryis set (enables delivery verification in tests)Test Suite (
test-apps/telemetry)--telemetry-filefor payload assertionsNODE_DEBUGstderr (sent (status=200))blocks-telemetryCLI consent commandsCI
Telemetry E2Ejob with AWS credentials, 60-min timeout, stack cleanupBLOCKS_STACK_SUFFIX)