Send telemetry in minutes.
Bolt Sight ingests standard OpenTelemetry (OTLP) over HTTP. Create a project and an ingest key, point an exporter at one endpoint and add one header.
Quickstart
Five steps from nothing to a span in the console. The base URL for every example is https://bsight.techsstuff.com.
- Create an account and a project. Sign up with a name, email, password and a name for your first project, or continue with Google. The project holds your telemetry, keys, members and credits.
- Check the project’s credits. Ingest and queries draw on prepaid credits. A project with none available is blocked until a pack is bought: ingest answers 429 and console queries answer 402. Buy from the project’s Billing page; see pricing.
- Create an ingest key. Open Settings › API Keys and create one key per producer. The key is shown once. Owners can create keys.
- Point your exporter at the endpoint. Set the base URL and the
Authorizationheader; SDKs add/v1/traces,/v1/metricsand/v1/logsthemselves. - Look for a 204. The curl request sends one span. Open Investigate and choose the
quickstartservice to see it. Services appear fromservice.name; nothing needs registering.
# Send one span. Create the key in the app under Settings › API Keys.
$ BOLT_SIGHT_API_KEY=bsk_your_project_key
# NOW is the current time in nanoseconds
$ NOW=$(( $(date +%s) * 1000000000 )); END=$(( NOW + 120000000 ))
$ curl -sS -o /dev/null -w "%{http_code}\n" -X POST "https://bsight.techsstuff.com/v1/traces" \
-H "Authorization: Bearer $BOLT_SIGHT_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @- <<EOF
{"resourceSpans":[{
"resource":{"attributes":[{"key":"service.name","value":{"stringValue":"quickstart"}}]},
"scopeSpans":[{"spans":[{
"traceId":"5b8efff798038103d269b633813fc60c","spanId":"eee19b7ec3c1b174",
"name":"GET /hello","kind":2,
"startTimeUnixNano":"$NOW","endTimeUnixNano":"$END",
"status":{"code":1}
}]}]
}]}
EOF
204# Any OpenTelemetry SDK, in any language
OTEL_SERVICE_NAME=billing-worker
OTEL_EXPORTER_OTLP_ENDPOINT=https://bsight.techsstuff.com
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
OTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer%20bsk_your_project_key
OTEL_RESOURCE_ATTRIBUTES=deployment.environment=production,service.version=1.4.2// Node.js SDK: call init() once, in its own module, before any other import.
// Install steps are on the Developers page in the app.
init({
endpoint: "https://bsight.techsstuff.com",
apiKey: process.env.BOLT_SIGHT_API_KEY!, // bsk_…, one key per producer
serviceName: "checkout-api",
serviceVersion: process.env.APP_VERSION,
environment: "production",
});
// Flush buffered telemetry before the process exits.
process.on("SIGTERM", () => void shutdown().finally(() => process.exit(0)));exporter, err := otlptracehttp.New(ctx,
otlptracehttp.WithEndpointURL("https://bsight.techsstuff.com/v1/traces"),
otlptracehttp.WithHeaders(map[string]string{
"Authorization": "Bearer " + os.Getenv("BOLT_SIGHT_API_KEY"),
}),
)
if err != nil { return err }
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter),
sdktrace.WithResource(resource.NewWithAttributes(semconv.SchemaURL,
semconv.ServiceName("billing-worker"),
)),
)
otel.SetTracerProvider(tp) // call tp.Shutdown(ctx) on exit# pip install opentelemetry-sdk opentelemetry-exporter-otlp-proto-http
provider = TracerProvider(resource=Resource.create({"service.name": "billing-worker"}))
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter(
endpoint="https://bsight.techsstuff.com/v1/traces",
headers={"Authorization": f"Bearer {os.environ['BOLT_SIGHT_API_KEY']}"},
)))
trace.set_tracer_provider(provider)Endpoints
One origin for everything: https://bsight.techsstuff.com. The hosted endpoint accepts OTLP over HTTP; use protobuf (application/x-protobuf) or JSON (application/json), uncompressed or with Content-Encoding: gzip.
| Endpoint | Signal | Body |
|---|---|---|
POST /v1/traces | Spans | OTLP ExportTraceServiceRequest. Server spans, client spans and exception events all arrive here. |
POST /v1/metrics | Metrics | Gauge, sum and histogram data points. Exponential histograms and summaries are not stored. |
POST /v1/logs | Logs and product events | Log records with severity, body, attributes and optional trace and span IDs. |
POST /v1/job-events | Job lifecycle events | JSON only, up to 1,000 events per request. See job events. |
Authentication and keys
Ingest keys are the only credential an exporter needs.
| Header | Authorization: Bearer bsk_… on every request. |
|---|---|
| Scope | A key belongs to one project; everything sent with it is stored in that project. |
| Permission | Write-only. A key can send telemetry. It cannot read data or sign in. |
| Storage | The key is shown once when you create it. Only a SHA-256 hash and a short prefix are kept, so a lost key cannot be recovered; create a new one. |
| Revocation | Revoking a key takes effect on the next request. To rotate, deploy the new key first, then revoke the old one. |
| One per producer | Create a key for each browser app, server runtime and gateway. Rate limits are applied per key and signal, so a noisy producer cannot starve another. |
| Browser keys | A key in a browser bundle is public by design. Give the browser its own key and restrict it with allowed origins. When the list is empty, requests from any origin are accepted. |
| Who creates keys | Project owners and administrators. |
More request examples
The same request shape for logs, metrics and job events. A 204 means accepted. Example data only.
# NOW is the current time in nanoseconds
$ NOW=$(( $(date +%s) * 1000000000 )); END=$(( NOW + 120000000 ))
$ curl -sS -o /dev/null -w "%{http_code}\n" -X POST "https://bsight.techsstuff.com/v1/logs" \
-H "Authorization: Bearer $BOLT_SIGHT_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @- <<EOF
{"resourceLogs":[{
"resource":{"attributes":[{"key":"service.name","value":{"stringValue":"quickstart"}}]},
"scopeLogs":[{"logRecords":[{
"timeUnixNano":"$NOW","severityNumber":17,"severityText":"ERROR",
"body":{"stringValue":"payment failed: card declined"},
"traceId":"5b8efff798038103d269b633813fc60c","spanId":"eee19b7ec3c1b174"
}]}]
}]}
EOF
204# NOW is the current time in nanoseconds
$ NOW=$(( $(date +%s) * 1000000000 )); END=$(( NOW + 120000000 ))
$ curl -sS -o /dev/null -w "%{http_code}\n" -X POST "https://bsight.techsstuff.com/v1/metrics" \
-H "Authorization: Bearer $BOLT_SIGHT_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @- <<EOF
{"resourceMetrics":[{
"resource":{"attributes":[{"key":"service.name","value":{"stringValue":"quickstart"}}]},
"scopeMetrics":[{"metrics":[{
"name":"quickstart.queue.depth",
"gauge":{"dataPoints":[{
"timeUnixNano":"$NOW","asDouble":42,
"attributes":[{"key":"queue","value":{"stringValue":"invoices"}}]
}]}
}]}]
}]}
EOF
204$ curl -sS -o /dev/null -w "%{http_code}\n" -X POST "https://bsight.techsstuff.com/v1/job-events" \
-H "Authorization: Bearer $BOLT_SIGHT_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @- <<EOF
{"events":[{
"schema_version":"1.0.0",
"event_id":"0b9f6f2e-8a53-4c8e-9c3f-3f2d1f3a9b11",
"source_timestamp":"2026-09-30T10:15:00Z",
"source_system":"bullmq","source_service":"billing-worker",
"environment":"production","queue_name":"invoices","job_name":"send-invoice",
"job_id":"4821","job_run_id":"4821-1","attempt":1,"state":"completed"
}]}
EOF
204Response codes
Every ingest response is an HTTP status. Retry 429 and 503 with backoff; fix the request for the others.
| Status | Meaning and what to do |
|---|---|
| 204 | Accepted. |
| 400 | The body is not valid OTLP, or its gzip stream is corrupt. Send protobuf or JSON, optionally gzip-compressed. JSON trace and span IDs are hex strings. |
| 401 | Missing, unknown or revoked API key. Check the Authorization: Bearer header and the key. |
| 403 | The browser origin is not in the project’s allowed origins. Add the exact origin, for example https://app.example.com. |
| 413 | Body over 4 MB, before or after decompression, or more than 10,000 records. Lower the exporter batch size. |
| 415 | Unsupported Content-Encoding. Use gzip or no compression. |
| 429 | Ingest rate limit reached, or ingest blocked because the project has no credits. Retry after the Retry-After header; check the project’s Billing page if it persists. |
| 503 | Temporarily unavailable. Retry with backoff. |
Limits
What the receiver enforces. Data over a per-record limit is bounded or dropped, not rejected.
| Per request | 4 MB after decompression and 10,000 records. |
|---|---|
| Per record | Up to 64 attributes (the rest are dropped), attribute keys up to 128 bytes, attribute values up to 1 KB, log bodies up to 8 KB, span, service and metric names up to 256 bytes, up to 64 events and 64 links per span. |
| Rate, per key and signal | Defaults per minute: 240 requests or 64 MiB. Requests with the same key and signal share a lane; traces, metrics and logs use separate lanes. |
| Rate, per project | Defaults per minute: 2,880 requests or 768 MiB across all keys and signals. |
| Keys and origins | Up to 50 ingest keys and 20 allowed origins (each up to 256 characters) per project. |
| Job events | Up to 1,000 per request; JSON over HTTP only. |
| Browser SDK | A per-tab cap on new traces: 60 per minute, a burst of 20 and 300 per page lifetime by default; values can only be lowered. |
SDKs
The Node.js and browser packages are conveniences; standard OpenTelemetry works without them. Install steps are on the Developers page in the app, after you sign in. Email support@techsstuff.com if you cannot reach it.
| SDK | What it does | Notes |
|---|---|---|
| Node.js | A thin wrapper around the OpenTelemetry Node SDK. init() configures trace, metric and log exporters, automatic instrumentation for HTTP and common frameworks and databases, and turns console output into log records with the active trace ID. shutdown() and forceFlush() flush before exit or at the end of a serverless call. | Options include endpoint, apiKey, serviceName, serviceVersion, environment, enabled, debug, sampler, propagation, an attribute policy and per-package instrumentation overrides. |
| Browser | initBrowser() reports Web Vitals, page-load spans and XMLHttpRequest spans. tracedFetch traces only requests that match an exact origin, method and path policy. Uncaught errors are opt-in, and error messages and stack traces are off by default. | Options include endpoint, apiKey, serviceName, sample rate, root-span budget, ignore URLs and error capture. |
| React and Next.js | A useBoltSight() hook, navigation spans reduced to your route templates so IDs in URLs never reach telemetry, error reporting, and Next.js (App Router) support. | Same options as the browser SDK. |
| Any other language | Use the OpenTelemetry SDK of the language with the endpoint and header above. No Bolt Sight package is needed. | Go, Python, Java, .NET, Ruby, PHP or a Collector. See Integrations. |
Webhooks
Add a webhook channel under Alerts › Channels and attach it to alert rules. Bolt Sight sends a JSON POST when a rule fires and again when it resolves.
{
"rule_name": "Checkout p95 latency",
"description": "p95 above 800 ms for 5 minutes",
"severity": "critical",
"state": "firing",
"value": 912.4,
"fired_at": "2026-09-30T10:30:00Z",
"labels": { "service": "checkout-api" },
"annotations": {}
}severityisinfo,warningorcritical;stateisfiringorresolved.value,resolved_at,labelsandannotationsare included when available.
Each request carries X-Webhook-Signature: sha256=<hex>, an HMAC-SHA256 of the raw body with the channel’s signing secret. The secret is generated when you create the channel (or you can supply your own, at least 16 characters) and is shown only once. Verify against the raw bytes: parsing and re-serializing the JSON changes them. The Node.js SDK also ships a verification helper.
import hashlib, hmac
def verify(body: bytes, signature: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
return hmac.compare_digest(signature, expected)func verify(body []byte, signature, secret string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(body)
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(signature), []byte(expected))
}Product events
Emit a log record whose body is the event name and put its dimensions in attributes, from the browser SDK or from any OpenTelemetry logs SDK. The Events page understands these names.
| Event (log body) | Attributes | Used for |
|---|---|---|
section_impression | section.id, section.position | Section click-through |
section_click | section.id, section.position | Section click-through |
upsell_click | upsell.surface | Funnel |
upsell_conversion | upsell.surface, conversion.type, payment.amount, payment.currency | Funnel and revenue |
rec_impression | rec.source | Recommendations |
rec_click | rec.source | Recommendations |
rec_watch_through | rec.source | Recommendations |
// Browser SDK: a product event is a log record whose body is the event name.
getLogger("product-events").emit({
body: "upsell_conversion",
attributes: {
"upsell.surface": "paywall",
"conversion.type": "subscription",
"payment.amount": "9.99", // decimal string
"payment.currency": "USD", // ISO 4217, upper case
},
});Job events
The Jobs page follows each run of a background job through its states. Post a JSON event for every transition (enqueued, active, retry_scheduled, completed, failed, stalled and others) to /v1/job-events, up to 1,000 per request; the request examples include one. Add trace_id and span_id to link a run to its trace. source_system accepts bullmq only today; job events are not accepted over gRPC.
Troubleshooting
| Nothing shows up | Confirm the endpoint and key with the curl request, then check the time range in the top bar. Data appears within a few seconds of export. |
|---|---|
| Browser requests fail with CORS or 403 | Add the page’s exact origin to the project’s allowed origins. |
| Frontend and backend spans are separate traces | Your API must accept the traceparent header (CORS Access-Control-Allow-Headers), and the call must go through tracedFetch or XMLHttpRequest. |
| A Node.js service has no spans | The instrumentation module must be imported before any other module. |
| Telemetry stops after a while | Check the project’s credits on the Billing page. With none available, ingest answers 429. |
| Still stuck | Set debug: true in the Node.js SDK to print exporter errors to the console, then contact us with the status code and what you expected. |