view .md
How to · Grow

Monitoring: one trace ID from extension click to HubSpot 200.

An HS-X request crosses three trust boundaries before it returns a value to the user. Each boundary used to drop the context — a UI extension log, a Worker log, and a HubSpot API response with no thread tying them together. HS-X stamps a trace ID at the extension and propagates it the whole way, so one search shows the full chain. This guide walks through reading the logs live, alerting on signals that wake you up only when something is actually wrong, and getting from a customer message to the failing line in under two minutes.

Time
≈ 10 min
Outcome
Structured logs streaming live, every request stamped with a trace ID that links UI extension to Worker to HubSpot API call, alerts wired to the four signals that matter, and a debug workflow that takes a user report to the failing log line in under two minutes.
Prerequisites
  • A deployed HS-X project — if you do not have one, run through /docs/guides/getting-started first.
  • The hs-x CLI installed and logged in with a connected Cloudflare account (the logs API is account-scoped).

Before you begin

A live request inside an HS-X project crosses three trust boundaries. The first is the HubSpot iframe — your UI extension is sandboxed JS running inside HubSpot's chrome. The second is the public edge — the extension calls runServerless(), which goes out to your Cloudflare Worker on its own URL. The third is the HubSpot API itself — the Worker turns around and calls api.hubapi.com with the portal's access token. Three processes, three networks, three log streams.

The thing that used to make this painful is that each hop traditionally dropped the context. You'd see console.log('saved') in the extension's devtools, then a separate Worker log somewhere, then a HubSpot rate-limit response with nothing tying them together. Reproducing a user's bug meant guessing which Worker invocation matched their click.

Today, the Worker side gives you a structured ctx.logger you can stamp with whatever request key makes sense — contactId, sync row id, or HubSpot's own correlationId (returned on error responses). End-to-end trace propagation from the UI extension through runServerless into outbound HubSpot calls is a planned surface; see the design-preview callout in step 3.

The three places logs live

EnvironmentWhere logs landHow you read them
Local dev (hs-x dev)Dev-server tail in your terminalAlready streaming. Cross-link: dev mode guide.
Deployed (any env)Cloudflare Workers logs + HS-X log bufferhs-x logs --project-id <id> --json
External APMOTLP endpoint you configureDesign preview — see step 5.

The dev-server tail and hs-x logs are always on. External APM export is on the roadmap (step 5). For now, logs live in Cloudflare's tail and in the HS-X log buffer.

Read live logs in dev

When you run hs-x dev, the CLI starts a local proxy that tails three streams at once: the Worker process (running locally under workerd), the UI extension bundle's dev console (forwarded over a WebSocket from the iframe), and the HubSpot API responses (intercepted by the SDK's http client). All three land in your terminal, interleaved by wall-clock time, prefixed with the source.

hs-x dev
Expect
$ hs-x dev
→ worker.ts        bundled · 84 kb
→ extensions       1 bundle · ws tail attached
→ dev portal       connected · my-portal-dev

[14:02:11.412] uix    trace=abc123  ContactPanel mounted
[14:02:11.480] worker trace=abc123  runServerless listDeals input={ contactId: 8801 }
[14:02:11.612] http   trace=abc123  GET /crm/v3/objects/deals 200 · 132ms
[14:02:11.621] worker trace=abc123  runServerless listDeals ok · 4 rows · 209ms

The trace=abc123 column is the same id you'll search for in the deployed logs. In dev, the SDK shortens it to six characters; in prod, it's a full ULID.

What the dev tail captures

  • UI extension console.log / console.warn / console.error calls, including stack traces from thrown errors.
  • Every runServerless invocation with its input parameters and return value.
  • Every outbound HubSpot API call with method, path, status, and latency.
  • Sync runs, including the row count, error count, and duration.
  • Any ctx.logger.info(...) you write explicitly via the handler context's structured logger.

For more on what the dev environment is doing under the hood — hot reload, the WebSocket tunnel, schema retypechecking — see the dev mode guide.

Common dev-log issues

  • UI extension logs missing. The iframe-to-CLI WebSocket can be blocked by aggressive ad blockers or strict corporate proxies. Check the browser devtools console for a ws://localhost:5174 connection error.
  • Logs interleaved out of order. The dev tail sorts by the timestamp at the source, not arrival. If your laptop clock drifts more than a second from the Cloudflare edge, you'll see UI logs appear after the Worker logs they triggered. Run sudo sntp -sS time.apple.com (or your OS equivalent) and reload.

Read live logs in deployed environments

For anything that isn't on your laptop, use hs-x logs. It opens a stream against Cloudflare's tail API for your project's Worker and prints structured events.

hs-x logs --project-id <id>
hs-x logs --project-id <id> --json

The two flags shipped today are --project-id <id> (required to pick the Worker) and --json (one structured event per line, ready to pipe).

JSON output and jq

Pass --json and pipe the stream into jq.

hs-x logs --project-id my-project --json \
  | jq -r 'select(.status >= 500) | "\(.timestamp) \(.method) \(.path) \(.status)"'
Design preview

A richer filter surface — --since, --follow, --trace, --filter "level=error,source=worker", --sample 0.1 — is on the roadmap. Today, filter client-side with jq against --json output, or browse the Cloudflare Workers dashboard for the project's Worker directly.

Common log-stream issues

  • hs-x logs shows nothing for a recently deployed Worker. Cloudflare's tail takes ~30 seconds to attach after a deploy. If nothing arrives after a minute, confirm your Cloudflare account is connected (re-run hs-x login) and that the project id is correct.
  • Old events you wanted are gone. The Cloudflare tail is best-effort and not retained. Use Cloudflare's dashboard for the Worker if you need a longer window than the live tail.

Trace one request end-to-end

On the Worker side, every action handler receives a HandlerContext that includes a structured logger. Anything you log through it lands in hs-x logs --json as a structured event you can grep.

export default defineApp({
  actions: {
    listDeals: async (ctx, { contactId }) => {
      ctx.logger.info('listDeals start', { contactId });
      const deals = await ctx.hubspot.crm.deals.list({ associatedContactId: contactId });
      ctx.logger.info('listDeals done', { count: deals.length });
      return deals;
    },
  },
});

When a HubSpot call fails, the SDK surfaces HubSpot's own correlationId (returned in error response bodies and as the x-hubspot-correlation-id response header). Log that id — it's the handle HubSpot Support uses to find the request in their access logs.

try {
  return await ctx.hubspot.crm.deals.list({ associatedContactId: contactId });
} catch (err) {
  ctx.logger.error('hubspot deals.list failed', {
    correlationId: err.correlationId,
    status: err.status,
  });
  throw err;
}
Design preview

A full end-to-end trace surface — a traceId minted at the UI extension, propagated through runServerless to the Worker, then onto outbound HubSpot calls — is on the roadmap. Today, correlate per-request by logging contactId (or whatever the natural request key is) plus HubSpot's correlationId on errors, and search the JSON stream.

Set up alerts on the four signals that matter

Alerting on "any error" trains you to ignore alerts. Alerting on p99 latency wakes you up because one user on a slow network exists. The four signals below are the ones that, when they cross a threshold, almost always indicate a real problem the user will see.

Design preview

A first-class alerts block on defineApp(...) config — declarative rules that hs-x deploy would provision against Cloudflare's analytics engine — is documented as the intended surface but is not yet shipped. The shape below illustrates the planned API.

// hsx.config.ts (planned)
defineApp({
  alerts: {
    notify: 'pagerduty:my-service-key', // or slack:#alerts, email:oncall@co.com
    rules: [
      { name: 'sync failure rate',     expr: 'sync.errors / sync.runs > 0.05',                          window: '15m' },
      { name: 'worker 5xx rate',       expr: 'http.server.5xx / http.server.total > 0.01',              window: '5m'  },
      { name: 'hubspot api error rate',expr: 'http.client.hubspot.status>=400 / http.client.hubspot.total > 0.02', window: '10m' },
      { name: 'sync latency p95',      expr: 'sync.duration.p95 > 30s',                                 window: '30m' },
    ],
  },
});

Until the surface ships, wire alerts directly in Cloudflare's dashboard against the Worker for your project.

Why these four, and not the obvious alternatives

SignalWhy it's the right one
Sync failure rateA sync that fails 100% wakes you up. A sync that fails 5% is a real signal — usually a rate limit or a malformed row that needs a fix, not a transient blip.
Worker 5xx rateThe Worker is your code. A 5xx is your bug. 1% is the right threshold; below that, you'll page on cold-start anomalies and isolate bugs.
HubSpot 4xx/5xx rateIncludes 429 (rate limited) and 5xx (HubSpot incident). Both are actionable: rate limits mean you need to back off harder, 5xx means check status.hubspot.com. 2% over 10 minutes filters single-request noise.
Sync latency p95p95 means "the worst case 1-in-20 users see." p99 fires on cold starts and is too noisy. p50 hides regressions. p95 is the sweet spot.

What not to alert on

  • Any single error event. One thrown exception in 24 hours is not a page-worthy signal; it's a Sentry issue.
  • Cold-start latency. Cloudflare Workers cold-start in single-digit ms, but the first request after a deploy can spike to 200ms once. Alerting on this teaches you to ignore the dashboard.
  • "Sync completed" as a positive event. Heartbeats belong in a dashboard, not in an alert channel.

Export traces and metrics to an external APM

Design preview

OTLP export — a telemetry block on defineApp(...) that forwards traces and metrics to Honeycomb, Datadog, Grafana Cloud/Tempo, or any OTLP/HTTP receiver — is planned but not shipped. No OTLP exporter, sampler, or service_name configuration exists in the CLI or runtime today.

In the meantime, if you need an external APM, use the Cloudflare Workers Logpush integration to forward Worker logs to your provider; that path is supported at the Cloudflare layer independently of HS-X.

Resolve common monitoring issues

The handful of issues that come up most often, with the fix that works.

Logs not appearing in hs-x logs

Two causes, ordered by frequency. First, the Cloudflare tail handshake hasn't completed yet — wait 30 seconds after a deploy. Second, your Cloudflare account isn't connected or the project id is wrong; re-run hs-x login and double-check --project-id.

Alert noise

If you're getting paged more than once a week and the page isn't a real incident, the threshold is wrong, not the alert. Widen the window, raise the threshold, and require the breach to hold for several minutes before firing.

Secret values accidentally logged

Don't log full token values, raw HubSpot responses, or unfiltered request bodies. When you need to log something derived from a token (for example, to confirm which token was used), log a short prefix or suffix — token.slice(0, 6) + '…' — never the whole value. The same goes for refresh tokens, email addresses, and customer PII.

ctx.logger.info('hubspot call ok', {
  tokenHint: ctx.token.access?.slice(0, 6),
  status: response.status,
});
Design preview

A first-class redact() SDK helper plus a config-level redaction field list is planned. Today, redact at the call site by selecting just the fields you actually need to log.

For deeper guidance on token and secret hygiene, see the secrets guide.

Where next