@openbarclay/telltale-client
v0.1.2
Published
Send in-app bug reports to a Telltale ingest Worker, which opens or comments on a GitHub issue in the project's own repo. Zero dependencies; works in browsers, React Native, Expo, Node and Deno.
Maintainers
Readme
@openbarclay/telltale-client
Send in-app bug reports to a Telltale ingest Worker, which scrubs them, deduplicates by fingerprint, and opens or comments on a GitHub issue in that project's own repository.
- Zero dependencies. Browsers, React Native, Expo, Node 18+, Deno — same package, no polyfill, no native module, no per-platform install step.
- Ships ESM and CommonJS.
importandrequireboth work, so Jest and other CJS tooling load it without anytransformIgnorePatternssurgery in your project. - ~4 kB. One
createTelltale()call and onereport(). - Implements every sender obligation in spec §8.1 so you don't re-derive them per app.
Crashes do not go here. They go Sentry → Sentry's native GitHub integration, with no Telltale code involved. This package is for user-typed bug reports only.
Install
npm install @openbarclay/telltale-clientUse
import { createTelltale, PUBLIC_POSTING_NOTICE } from '@openbarclay/telltale-client'
const telltale = createTelltale({
project: 'tenzy', // your registry slug
secret: process.env.EXPO_PUBLIC_TELLTALE_SECRET!, // YOUR project's secret, only
endpoint: 'https://telltale.openbarclay.workers.dev',
release: { version: '1.4.2', surface: 'android' }, // optional
})
// Fire-and-forget: never blocks, never throws.
await telltale.report({
title: 'Save button does nothing on the guide screen',
body: 'Tapped save, nothing happened, no error.',
})report() resolves to { ok: true, issue, url } or { ok: false, error, status } and never
rejects. Ignore the promise entirely if you don't need the issue number.
You must show the notice
Spec §4.3 requires a submit-time notice, and this package cannot enforce it — only your UI can place it:
<Text style={styles.notice}>{PUBLIC_POSTING_NOTICE}</Text>
<Button title="Send report" onPress={submit} disabled={!title.trim()} />The server-side scrub is pattern-based. It catches well-formed emails and phone numbers, not
alex at example dot com. The notice is the user's only real warning.
The notice deliberately does not say who can read the issue. §4.3 was written assuming public repositories, but every repo in the registry is currently private — so "anyone can read this" would be false. It cannot claim the reverse either: repository visibility is an operator setting, and flipping a repo to public exposes every historical report retroactively, with no notice to the people who filed them. The wording asserts only what holds under both: the report is filed, durable, and should not carry personal details.
React Native / Expo
Pass AsyncStorage so the anon_id survives app restarts. Without it the id is session-scoped, which
still works — it just stops grouping one user's repeat reports.
import AsyncStorage from '@react-native-async-storage/async-storage'
const telltale = createTelltale({
project: 'tenzy',
secret: process.env.EXPO_PUBLIC_TELLTALE_SECRET!,
endpoint: 'https://telltale.openbarclay.workers.dev',
storage: AsyncStorage, // satisfies the interface as-is, no adapter
})No crypto polyfill is needed. React Native ships no SubtleCrypto — Hermes has no WebCrypto, and
expo-crypto exposes digests rather than HMAC — so this package carries its own SHA-256/HMAC and
uses it automatically when WebCrypto is absent. Both paths are asserted byte-identical to the
Worker's own verifier in the test suite; that cross-check is the reason you can trust the fallback.
Use 0.1.2 or later on React Native. 0.1.0 and 0.1.1 never delivered a report from iOS or
Android. React Native replaces the global AbortSignal with a polyfill that has no static
timeout, so the request deadline threw before fetch ran. Every native report then came back
network_error, 2 seconds late, while web builds worked. 0.1.2 falls back to an
AbortController plus a timer when AbortSignal.timeout is missing. npm run verify:client now
sends a report through each built half with both AbortSignal.timeout and WebCrypto removed, as on
Hermes.
Browser
Nothing extra. localStorage and crypto.subtle are picked up automatically.
About the secret
The secret ships inside your client and is extractable. This is a known, accepted position (spec §4.1, §10.1), not an oversight. HMAC here does two things:
- Raises abuse from a one-line
curlto reverse-engineering a specific app. - Makes abuse attributable to one project, so the response is rotating exactly one secret.
It is not a confidentiality boundary. Give each app only its own secret — never the whole
TELLTALE_SENDER_SECRETS blob — because X-Telltale-Project is the sole authority for which repo
gets written to, and a shared secret would let the weakest client publish into any repo in the
registry.
API
createTelltale(options)
| Option | Required | Notes |
|---|---|---|
| project | ✓ | Registry slug. Must exist in the Worker's src/registry.ts. |
| secret | ✓ | This project's HMAC secret. |
| endpoint | ✓ | Worker origin; a full /v1/events URL also works. |
| release | | { version, surface }, surface ∈ ios\|android\|web\|desktop. Default for every report. |
| context | | { platform, os_version, locale } — a bounded set; other keys are dropped server-side. |
| storage | | Defaults to localStorage, else memory. Pass AsyncStorage on RN. |
| fetch / now | | Injectable for tests. |
| retryDelayMs | | Defaults to 2000. §8.3 mandates this; lowering it deviates from spec. |
| onError | | Transport failures. Without it, the only signal is the Worker's /v1/stats. |
report(report)
{ title, body?, release?, context?, occurredAt?, diagnostics? }. title is trimmed and truncated
to 120 chars; body is truncated server-side at 8000. Neither is ever rejected for length.
Attaching logs — diagnostics
GitHub has no API for issue attachments, so a log tail is rendered inline instead: the Worker puts
it in a collapsed <details> block after the user's own text, under its own 4000-char budget so it
cannot truncate the part a human wrote.
await telltale.report({
title,
body,
diagnostics: recentLogLines.join('\n'), // a curated tail, not raw console
})It gets the body-scope scrub plus home-path redaction — C:\Users\alex\… becomes
C:\Users\[redacted:user]\…, preserving the path shape for triage while dropping the account name.
That rule exists because a home path is by far the most common identifier in machine-generated text,
and neither the email nor the phone pattern catches it.
Show the user what you are attaching, before it is sent. They can self-censor what they type; they cannot review what your app attaches, and the issue is durable. The scrub is pattern-based — it will not catch API tokens, session ids, or file names someone chose. Send a curated log tail, never raw console output.
anonId()
The install-scoped, per-project anonymous id, generated and persisted on first use. Client-controlled and regenerable — explicitly never a security boundary (§4.2).
Errors you can get back
| error | Meaning |
|---|---|
| empty_title | Caught locally before any request. |
| bad_signature | Wrong secret, or the body was re-encoded after signing. |
| clock_skew | Device clock is >10 min out and the single re-sign retry also failed. |
| unregistered_project | The slug is not in the Worker's registry — usually a typo. |
| rate_limited | §4.2 caps. Dropped, not queued. |
| ignored | Matched an issue marked not_planned or telltale:muted. Working as intended. |
| github_error | GitHub unreachable — or a PAT that is unset or expired. |
| labels_dropped | The issue was created but its labels were not. Breaks dedup; operator must look. |
| network_error | Both attempts failed to reach the Worker. |
Retry behaviour
One retry, total. Spec §8.3 allows a single retry after 2s, then the report is dropped — there is no persistent outbox, because that means durable on-device storage of user-typed text.
The clock-skew re-sign is that retry, not an extra one. On a 401 carrying
X-Telltale-Server-Time the client re-signs with the server's clock and retries immediately; on a
5xx it waits (honouring a sane Retry-After, capped at 30s) and retries once. A 400, 404 or
429 is never retried.
Adding a new project
- Add an explicit entry to the Worker's
src/registry.ts— there is no slug-to-repo inference, because a wrong guess writes a user's bug report into a stranger's repository. - Generate an HMAC secret, add it to
TELLTALE_SENDER_SECRETS, redeploy the Worker. - Give the app that one secret and call
createTelltale.
License
MIT
