Skip to content

feat(telemetry): e2e test suite + create-blocks-app worker parity + cross-platform console#133

Open
sarayev wants to merge 44 commits into
mainfrom
feat/telemetry-e2e-tests-clean
Open

feat(telemetry): e2e test suite + create-blocks-app worker parity + cross-platform console#133
sarayev wants to merge 44 commits into
mainfrom
feat/telemetry-e2e-tests-clean

Conversation

@sarayev

@sarayev sarayev commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

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

  • create-blocks-app: Port telemetry to detached subprocess worker (matches core), remove CI suppression
  • console: Cross-platform opener (macOS/Linux/Windows) + read region from AWS_REGION
  • core: Conditional stderr inherit when NODE_DEBUG=blocks-telemetry is set (enables delivery verification in tests)

Test Suite (test-apps/telemetry)

  • Invokes real CLI scripts (not mocks) with --telemetry-file for payload assertions
  • Verifies actual delivery to the telemetry endpoint via NODE_DEBUG stderr (sent (status=200))
  • Covers: dev, sandbox, sandbox:destroy, deploy, destroy, console, create-blocks-app, vendorize
  • Tests SUCCESS + FAIL paths for each command (cloud commands use real CDK deploys with valid creds)
  • Disable mechanisms (env var, global config, project config)
  • Network resilience, schema validation, privacy, identifier stability
  • blocks-telemetry CLI consent commands

CI

  • New Telemetry E2E job with AWS credentials, 60-min timeout, stack cleanup
  • Unique stack names per run (BLOCKS_STACK_SUFFIX)
  • Pinned installation ID for deterministic assertions

sarayev added 18 commits July 9, 2026 08:57
…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.
svidgen
svidgen previously approved these changes Jul 9, 2026
if (config.telemetry?.enabled === true) return true;
} catch { /* no config */ }

if (isCI()) return false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we distinguish between "human" and CI use in the dashboard?

@sarayev sarayev Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we can. Here, this check was preventing from collecting events in ci for the create-blocks-app command, which I am removing now.

@changeset-bot

changeset-bot Bot commented Jul 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 47ef470

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@aws-blocks/core Patch
@aws-blocks/create-blocks-app Patch

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

@soberm soberm left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed commit 88d9373. Production fixes look correct; main feedback is on the duplicate worker, the CI suppression removal, and a handful of test/doc cleanups. Nothing blocking.

Comment thread packages/create-blocks-app/src/telemetry-send-worker.ts
Comment thread packages/create-blocks-app/src/telemetry-send-worker.ts
Comment thread packages/create-blocks-app/src/telemetry.ts
Comment thread packages/create-blocks-app/src/telemetry.ts Outdated
Comment thread packages/core/src/scripts/console.ts
Comment thread package.json
Comment thread test-apps/telemetry/README.md Outdated
Comment thread test-apps/telemetry/test/telemetry-e2e.test.ts Outdated
Comment thread test-apps/telemetry/test/telemetry-e2e.test.ts Outdated
- 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
@sarayev

sarayev commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

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.

@ahmedhamouda78 ahmedhamouda78 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

8 comments below: 2 major, 3 minor, 3 nits. The two majors are the ones I'd resolve before merge; the rest are suggestions/nits.


/** 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)}`);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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', () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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'],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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\)/;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@sarayev

sarayev commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the detailed review! Dispositions: the two majors (#1 prod endpoint, #2 || true teardown) are pre-existing repo-wide conventions shared by the other e2e jobs (evidence in-thread), so I'd address those repo-wide as a follow-up rather than in this PR. #3 (CI suppression) is a bug fix aligning create-blocks-app with already-documented behavior — no customer-facing wording needed. The remaining items (#4 worker drift guard, #5 watchdog, #6 NODE_DEBUG helper, #7 Windows start, #8 local delivery sink) are bundled into a tracked follow-up; #8 (delivery-assertion flakiness) is the one I most want to land.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants