tailthemesbuy this theme · $39
Skip to content

Design system

The parts

Every part Signal ships, rendered once with real content: 12 sections, 12 primitives, 45 tokens per mode.

12 sections12 primitives45 tokens2 modes

src/components · src/components/ui · src/theme.css · src/fonts.ts

Tokens

45 variables per mode in src/theme.css: 18 colour, 24 type, three shape and weight. These chips read them live.

Surfaces and pairs

  • Aa

    --background

    --foreground

  • Aa

    --card

    --card-foreground

  • Aa

    --primary

    --primary-foreground

  • Aa

    --secondary

    --secondary-foreground

  • Aa

    --muted

    --muted-foreground

  • Aa

    --accent

    --accent-foreground

State ramp

  • --primary

  • --primary-hover

  • --primary-active

  • --surface-tint

Type · ratio 1.200

--font-text · IBM Plex Sans

Aa Bb Cc 0123

--font-mono · IBM Plex Mono

Aa Bb Cc 0123

  • d4 · 2.592rem · h1Deploy-aware observability
  • d3 · 2.160rem · section h2Built for how you already debug
  • d2 · 1.800rem · page h2What the tenth minute looks like
  • d1 · 1.500rem · h3Three lines to instrument
  • lead · 1.125remThe section lede, one sentence, twenty words at most.
  • body · 1remRunning prose: traces, logs and deploy events on one timeline.
  • small · 0.875remCard body, nav links, sidebar items, buttons.
  • caption · 0.75remChips, table cells, footnotes, mono chrome.
  • label · caption + 0.08emCoverage

Radius and lines

  • --radius · --radius-inner

    8px outer, 5px inner, never the other way round

  • --border

    one hairline weight, everywhere

  • --ring · --shadow-color

    focus

    2px ring offset 2 · shadows never use --background

Sections

Each section rendered once, at the width and with the content it ships with.

site-header

src/components/site-header.tsx

Sticky nav: wordmark, five links, a star chip and two buttons. Below md it collapses into a JS-free details menu.

terminal-hero

src/components/terminal-hero.tsx

The home masthead: badge, headline, install row and a live sgl watch transcript that overhangs the container.

v2.4

Every deploy lands on the same timeline as the latency it moved

When p95 moves, the commit that shipped 24 seconds earlier is already next to it.

brew install signalhq/tap/sgl

free for solo devs · no card · self-host the collector

~/payments — sgl watch
$ sgl watch --env prod --since 15m

  prod · us-east-1, eu-west-1 · 3 services

  ok  api-gateway      p95 184ms  err 0.02%  ▁▂▁▁▂▁
  ok  checkout         p95 221ms  err 0.05%  ▁▁▂▁▁▁
  !!  payments-worker  p95 912ms  err 1.31%  ▁▂▄▆██

  ▲ regression  payments-worker · p95 +611ms
    baseline    301ms (24h rolling)
    onset       14:02:11Z · 11m ago
    deploy      8f21c4e "retry budget for webhooks"
                @nadia · 24s before onset

  → sgl trace payments-worker --deploy 8f21c4e
prod · 3 services · 1 regression · refreshed 4s ago
sgl watch · live service health, deploy-annotated

incident-band

src/components/incident-band.tsx

The hero's regression narrative, drawn: a p95 series flat in its 24h envelope, the deploy marker, then the step up 24 seconds later. Inline SVG computed from two point arrays, token-coloured, server-rendered and static.

The regression, drawn: p95 against the deploy timeline

p95 301ms → 912ms at 14:02:11Z · deploy 8f21c4e (@nadia) landed 24s before onset

integrations

src/components/integrations.tsx

Eight data sources on a full-bleed hairline grid, one lucide glyph each, describing what the source does.

Signal reads what you already run

No proprietary agent: anything that speaks OTLP works the day you install it, and the rest pipes through sgl ingest.

  • OpenTelemetryOTLP traces + metrics
  • Prometheusscrape + remote write
  • Kubernetespod, node + owner labels
  • PostgreSQLslow query → span link
  • gRPCstreaming span capture
  • Envoysidecar access logs
  • Fluent Bitlog forwarding
  • systemdbare-metal units

feature-grid

src/components/feature-grid.tsx

Four cards carrying nine lines of real YAML, TypeScript and shell, a named outcome, then three supporting claims.

What you get

Built for how you already debug

Signal is a CLI first: everything you can click, you can also review in a pull request.

Deploys are first-class events

Every release lands on the timeline with its sha, author and diff. When a regression starts inside a deploy window, Signal attributes it before you open a dashboard.

.github/workflows/deploy.ymlyaml
# runs after your existing deploy step
- name: Mark deploy in Signal
  uses: signalhq/mark-deploy@v2
  with:
    service: payments-worker
    env: prod
    sha: ${{ github.sha }}
    by: ${{ github.actor }}
    fail-on-regression: true

Three lines to instrument

One import at the top of your entrypoint. The SDK wraps HTTP, gRPC and your database driver, and adds nothing to the request path but a buffer write.

instrument.tsts
import { init } from "@signal/node";

init({
  service: "payments-worker",
  env: process.env.SIGNAL_ENV,
  // all errors, half the slow ones, 1% of the rest
  sample: { errors: 1, slow: 0.5, baseline: 0.01 },
  endpoint: process.env.SIGNAL_ENDPOINT,
});

Alerts live in your repo

Thresholds are code, reviewed in pull requests, versioned with the service they watch. No click-ops, no drift between staging and prod.

signal.config.tsts
import { alert, p95 } from "@signal/config";

export default [
  alert({
    when: p95("checkout.submit").above("400ms"),
    for: "5m",
    notify: ["#oncall-payments", "escalate:tier1"],
  }),
];

Query traces like a table

The same query language in the CLI, the dashboard and the API. Pipe it to jq, commit it as a saved view, or page it into an incident doc.

zsh · sgl querybash
$ sgl query 'p95(latency) by route' \
    --service checkout --since 24h

route                 p95      n       24h
/checkout/submit     412ms   18.2k    +38%
/checkout/quote      121ms   44.9k     -2%
/checkout/receipt     88ms   12.1k     +1%
/checkout/webhook    118ms    6.4k     -1%
/checkout/refund     204ms    2.1k     +4%
“Time to attribution went from forty minutes to under three.”
Priya Raghavan · staff SRE, Kesteven Health · 140 services on the self-hosted collector

Sampling that keeps the weird ones

Head sampling throws away exactly the requests you needed. Signal decides after the fact: every error, every p99 outlier, a slice of the boring remainder.

sgl config sampling

Self-host in one binary

The collector is a single static binary: no queue, no cluster to babysit. On the self-hosted plan raw traces never leave your VPC.

sgl collector verify

A bill you can predict

Priced per ingested gigabyte after sampling, not per host and not per seat. Estimate it against your current volume before you commit.

sgl cost --estimateestimator

pricing-plans

src/components/pricing-plans.tsx

Three plans on one price model, then the definition of a billed gigabyte beside the ledger of things that are never metered.

Plans

Three plans, one meter

Ingested gigabytes after sampling is the only meter on the account.

Free

$0

Up to 5 GB a month after sampling. There is no expiry on it.

  • 30-day retention
  • Unlimited seats and services
  • All seven commands, alerts included
  • No card, no trial clock

Start free

Usage

$0.42/ GB

Every gigabyte after sampling. No minimum, no seat count, no host count.

  • 13-month retention
  • Deploy attribution and trace-linked alerts
  • sgl diff across releases and windows
  • First response in 4 business hours

Estimate your bill

Self-hosted

Aggregates only

The same $0.42 meter, on what the collector forwards. Raw spans never leave your VPC.

  • One static binary, no queue to run
  • Same seven commands, SIGNAL_ENDPOINT set
  • 13-month retention on what you forward
  • sgl collector verify before you cut over

Read sgl collector

What counts as a gigabyte

The collector measures the compressed bytes it accepts, after your sample rates have already thrown work away. A span you never sent is a span you never pay for.

Retention is included rather than metered: thirteen months on the usage plan, thirty days on the free tier. Storing a gigabyte for a year costs the same as storing it for a week.

Run sgl cost --by service to see which service is spending it.

Never on the invoice

seats
invite the whole team, and the contractors
services
one service per deployable, not per licence
hosts
scale to forty replicas at the same price
deploy events
mark every release, including the reverts
alert evaluations
the collector runs them, you do not pay per check
API and CLI calls
query as often as an incident needs

cost-estimator

src/components/cost-estimator.tsx

A client island. Raw volume and sample rate go in, the invoice arithmetic comes out; the default state is the same 107.1 GB and $44.98 the sgl cost transcript prints.

Estimator

Put your own volume in

Every figure below is the arithmetic on your invoice, not a range.

GB before sampling

Sample rate

Sampling is set per service in signal.config.ts. Every error and every p99 outlier is kept whatever the rate, so the slice you drop is the boring remainder.

raw spans
1,071.0 GB / month
sample rate
10%
ingested, billable
107.1 GB
rate
$0.42 / GB

Estimated month

$44.98

107.1 GB at $0.42/GB

Up to 5.0 GB a month after sampling costs nothing. Above that line every gigabyte is billed, including the first five.

Already sending spans? Run sgl cost --estimate and it reads the last thirty days off your own account.

contact-card

src/components/contact-card.tsx

The company surface: four contact routes with stated response times, a full-bleed operations band, and the legal minimum at #legal.

Support

Where to send what

Pick the one that matches the problem and it reaches an engineer, not a queue.

Engineering support

Instrumentation that will not attach, a query that returns nothing, a sample rate that reads wrong. Paste the output of sgl doctor and skip the first three replies.

support@signal.sh4 business hours

Security

Vulnerability reports and coordinated disclosure. Encrypt with the key at fingerprint 3C7A 55E1 90BD 4F26 if the report contains customer data.

security@signal.sh24 hours, any day

Production incidents

Ingest is dropping spans, alerts are not firing, the collector will not start. Usage-plan accounts page an on-call engineer directly.

incidents@signal.sh15 minutes, any hour

Billing

Invoices, VAT numbers, purchase orders and the one question everyone asks: which service spent the gigabytes. Include the invoice number.

billing@signal.sh1 business day

support and billing Mon to Fri, 09:00 to 18:00 CET · incidents and security any hour, any day

How it runs

Data residency
per service
eu-west-1 or us-east-1, chosen at creation and never moved
Retention
13 months
30 days on the free tier, deleted not archived
Sub-processors
2
AWS for compute and storage, Cloudflare for the edge
Ingest uptime
99.95%
measured at the collector, trailing 90 days

changelog-section

src/components/changelog-section.tsx

Dated release entries with icon-marked tags. Shown here in preview: variant='full' swaps the three latest for all eight, adds the migration transcript and the tag filter.

Changelog

Every release is in the log

216 releases since 2024, each written by the person who shipped it.

full changelog
v2.4.0

Deploy correlation for monorepos

One repository can now emit a deploy event per service, so a forty-package monorepo stops blaming every regression on the same commit.

  • addedmark-deploy@v2 takes a service matrix and fans out one event per changed package.
  • addedsgl trace --deploy <sha> scopes a trace search to a single release window.
  • improvedAttribution weights onset distance: a deploy 24s before onset now outranks one 9m before.
  • fixedRe-running a workflow no longer duplicates the deploy annotation.
v2.3.2

Sampling budget drift on long-lived workers

Processes that never restart drifted above their configured baseline rate, up to 4x on workers older than a week. Thanks to @tvoss for the reproduction.

  • fixedThe baseline sampler refills its token bucket on the hour instead of on process start.
  • fixedsgl doctor reports the effective sample rate, not the configured one.
v2.3.0

Trace-linked alerts

An alert now arrives carrying the three slowest traces that triggered it, so the notification is the first step of the investigation rather than a reason to go find one.

  • addednotify targets accept templates, including trace_url and deploy_url.
  • addedConditions can hold for up to 24h, for slow-burn error budgets.
  • improvedEvaluation moved into the collector: median notification latency 41s to 6s.

cta

src/components/cta.tsx

The closing panel: headline, two buttons, an init command and four answers to cost, risk and effort.

Your next regression already has a commit

Install the CLI, point one service at the collector, and the next deploy annotates itself.

sgl init --service payments-worker
  • $0.42 per ingested GB after sampling
  • Free under 5 GB a month, 30-day retention
  • No credit card, no sales call
  • Self-host the collector whenever you want

docs-shell

src/components/docs-shell.tsx

Page-scale: sidebar, article, on-this-page rail and the full-bleed payoff band. Shown here in variant='quickstart'; variant='cli' swaps in the seven-command reference and its own transcript.

Documentation menu

This guide takes a Node service from zero to a deploy-annotated latency timeline. It takes about ten minutes, and nothing here is reversible in a way that should worry you — the SDK is a no-op until an ingest key is present.

1

Install the CLI

The CLI is a single binary. It is the same tool your CI uses, so install it the same way in both places.

terminalbash
brew install signalhq/tap/sgl
# or: curl -fsSL https://get.signal.sh | sh

sgl --version
# sgl 2.4.0 (darwin/arm64)
2

Create a service

A service is the unit everything else hangs off: spans, alerts, deploys and cost. Create one per deployable, not per repo.

terminalbash
sgl login
sgl service create payments-worker --env prod

# wrote SIGNAL_INGEST_KEY to .env.local
# scoped to payments-worker/prod
# rotate with: sgl key rotate
3

Instrument it

The SDK wraps HTTP, gRPC and your database driver. It has to load before your application code, which is what the --import flag is for.

instrument.tsts
import { init } from "@signal/node";

init({
  service: "payments-worker",
  env: process.env.SIGNAL_ENV ?? "dev",
});
package.jsonjson
{
  "scripts": {
    "start": "node --import ./instrument.js dist/app.js"
  }
}

Running the collector yourself? Set SIGNAL_ENDPOINT before this step. In self-hosted mode the SDK never opens a connection to signal.sh — the checks to run first are under sgl collector.

4

Mark your deploys

This is the step that makes the rest of Signal worth having. Run it at the end of your deploy job, after traffic shifts.

.ci/deploy.shbash
sgl deploy mark \
  --service payments-worker \
  --env prod \
  --sha "$(git rev-parse HEAD)" \
  --by "$CI_ACTOR"
5

Watch it

Once a deploy and a few thousand spans are in, the timeline can attribute a change to a release. Add --json to the same command to gate a rollout in CI.

terminalbash
sgl watch --env prod --since 15m
sgl watch --env prod --since 15m --json | jq '.regressions'

What the tenth minute looks like

One deploy and a few thousand spans later, the timeline stops reporting and starts attributing.

~/payments — sgl watch --since 15m
$ sgl watch --env prod --since 15m

  ok  payments-worker  p95 301ms  err 0.04%  ▁▂▁▁▂▁

  ▲ regression  payments-worker · p95 +611ms
    baseline    301ms (24h rolling)
    onset       14:02:11Z · 11m ago
    deploy      8f21c4e "retry budget for webhooks"
                @nadia · 24s before onset

  → sgl trace payments-worker --deploy 8f21c4e
payments-worker · prod · 1 regression attributed

The last line is the next command. That is the whole product.

Primitives

The ui/ parts every section is built from, in one frame.

ui

src/components/ui/*.tsx

Every ui/ part in the states it ships with. ui/container, ui/type and ui/logo are structural: they render in the shell around this page.

ui/button · variant

ui/button · size

ui/theme-toggle · ui/copy-button

brew install signalhq/tap/sgl

ui/badge · ui/inline-code

sgl 2.4.0breakingsgl trace --deploy 8f21c4e

ui/code-block

instrument.tsts
import { init } from "@signal/node";

init({ service: "payments-worker", sample: { errors: 1 } });

ui/terminal-window

~/payments — sgl status
$ sgl status --env prod

  ok  api-gateway     p95 184ms
  !!  payments-worker p95 912ms

ui/section-heading

Alerts

Page the person who shipped it

Route by deploy author, not by rota. The commit already knows who to wake.

ui/page-hero

Docs

Quickstart

From an uninstrumented Node service to a deploy-annotated latency timeline.

sgl 2.4.0

counterweight slot

Every masthead fills its right half. A1, never the retired A2.

src/components/site-footer.tsx

Three link columns, the product blurb, a build string and a status line. Every link resolves to a page or an anchor the theme ships. Rendered in place as the page shell.