// docs

Issue trackers & notifications

When a tester marks Fail, Teasynaer can file a tracked issue with the evidence bundle into the tracker you configure. Credentials stay encrypted at rest.

When a tester marks a case Fail, Teasynaer can file a tracked issue — case details, notes and an evidence bundle attached — into whatever your team already uses. This page is for the admin wiring that up: which adapters exist, how to configure each one, what's encrypted and what isn't, and what happens when no tracker is set (or filing fails).

There's a second, smaller seam alongside it — notifications — covered at the bottom of this page. It only ever announces something happened; it never files an issue and never records a verdict.

AdapterSelf-host-readyConfigured viaNotes
GitHubYesEnv varsDefault (QA_TRACKER=github)
GitLabYesEnv varsWorks with gitlab.com or self-hosted
Jira CloudYesEnv varsCloud only — no Jira Server/Data Center adapter
WebhookYesEnv varsPOST the draft to any endpoint you control
Forgejo / GiteaYesEnv varsNeeds migration 0014 — see below
HulyNo — needs a bridgeAdmin wizard (DB, encrypted)See the caveat before you reach for this one
Export packetAlways onNothing to configureThe fallback — see below

All six sit behind one IssueTracker interface (src/lib/qa/tracker.ts), so switching adapters never touches the run/fail flow — only which adapter's fileIssue() gets called.

How filing works

On a Fail, the On-Fail screen gives you two buttons: "File issue + bundle →" and "Export packet." Filing:

  1. Builds a tracker-agnostic IssueDraft — case name, steps/expected, your notes — and never the tracker's own credentials.
  2. Gathers the evidence bundle that's already attached to the failing result (screenshots, the on-fail clip, HAR, console log) as attachments.
  3. Calls the configured adapter's fileIssue(draft, attachments).
  4. Saves the returned { ref, url } against the result and fires a (fire-and-forget) issue.filed notification if you've wired one up.

Re-clicking "File issue" on an already-filed result is a no-op — it returns the existing ref instead of filing a duplicate. Every filed-or-failed attempt is written to the audit log (/settings/audit).

The five self-host-ready adapters

These need a repo/project and a token — nothing else. All five are configured with environment variables today; there's no in-app credential form for them yet (that's tracked as post-MVP polish, see Post-MVP backlog below). Set the variables in .env (Docker) or .env.local (dev), restart, and the admin wizard at /settings/integrations/tracker will show a "Configured" chip for whichever destination you picked.

GitHub — the default

QA_TRACKER=github
QA_GITHUB_REPO=your-org/your-repo
QA_GITHUB_TOKEN=ghp_...            # PAT or fine-grained token with issues:write
QA_GITHUB_API_URL=                 # optional — set for GitHub Enterprise Server

GitLab

QA_TRACKER=gitlab
QA_GITLAB_PROJECT=123456           # numeric id, or "group/project"
QA_GITLAB_TOKEN=glpat-...          # personal/project access token, api scope
QA_GITLAB_URL=                     # optional — default https://gitlab.com; set for self-hosted

Jira Cloud

QA_TRACKER=jira
QA_JIRA_URL=https://your-org.atlassian.net
QA_JIRA_EMAIL=qa-bot@your-org.example
QA_JIRA_TOKEN=...                  # Atlassian API token (Basic auth: email:token)
QA_JIRA_PROJECT=OPS
QA_JIRA_ISSUE_TYPE=                # optional — default "Bug"

Jira Cloud only — there's no Jira Server/Data Center adapter.

Generic webhook

Posts the IssueDraft as JSON to a URL you own; expects { url, ref? } back. This is also how you'd bridge to a tracker that doesn't have a first-class adapter yet.

QA_TRACKER=webhook
QA_WEBHOOK_URL=https://your-service.example/teasynaer-issues
QA_WEBHOOK_TOKEN=                  # optional — sent as "Authorization: Bearer <token>"

Forgejo / Gitea

QA_TRACKER=forgejo
QA_FORGEJO_URL=https://git.your-org.example   # instance base — required, no default
QA_FORGEJO_REPO=your-org/your-repo
QA_FORGEJO_TOKEN=...                          # PAT with issue write access

Forgejo and its Gitea upstream share a GitHub-compatible /api/v1 Issues API, so this adapter is modelled directly on the GitHub one, with two honest differences worth knowing before you rely on it:

  • No labels yet. Forgejo/Gitea's create-issue endpoint wants numeric label IDs, not names, and resolving a label name to an ID needs an extra lookup this adapter doesn't do yet. labels is omitted from the request rather than sent broken.
  • No attachment upload. Like the other adapters, evidence files stay on disk under your evidence store; the filed issue carries the case/steps/notes text, not the binary files.

One extra step: the forgejo tracker kind was added by database migration 0014. If you're running an image or a checkout from before that migration shipped, QA_TRACKER=forgejo will fail because the database doesn't know the enum value yet — run your migrations up to date (docker compose runs them on boot; a bare checkout needs pnpm db:migrate) before switching to it.

Huly (needs an external bridge)

Read this before you reach for Huly. In v0.1, filing into Huly does not talk to Huly directly — it POSTs to a small HTTP bridge (qa-file-issue, POST /issues + POST /issues/:ref/attachments) that you run beside your Huly instance. That bridge is not part of this repository. A stock Teasynaer self-host cannot file into Huly until you stand one up yourself — that's why the shipped default is github, not huly.

If you already run that bridge, Huly is the one adapter with a proper in-app setup form — see the admin wizard below. Everyone else: use one of the five adapters above. Full detail, field mapping and the target (bridge-free) design are in huly-simple-setup.md.

Configuring: the admin wizard vs env vars

/settings/integrations/tracker is the one screen for all of this. Any logged-in user can open it and see what's configured (GET /api/settings/tracker — kind, configured, last test result); only an admin can change anything.

Pick a destination card and the wizard shows you one of two things:

  • Huly — an actual form: instance URL, workspace, access token, project key, an allow-private-network checkbox, a Test connection button, and Save. This is the only adapter stored in the database today.
  • GitHub / GitLab / Jira / Webhook / Forgejo — a read-only panel listing exactly which environment variables to set, plus a live "Configured · <kind>" chip once they're present. There's nothing to type here (yet) — you set these on the server and restart.
  • Export packet — no form at all. It's always available, credential-free.

Precedence: if a Huly config is saved in the database, it wins. Otherwise Teasynaer reads QA_TRACKER and that adapter's env vars. If neither is set, no tracker is configured and filing returns "not configured."

The admin wizard, Huly only

Saving Huly credentials in the UI requires TRACKER_CREDENTIALS_KEY to be set on the server — without it, the save fails closed (in production, hard closed; in development, an error tells you to set it in .env.local):

TRACKER_CREDENTIALS_KEY=$(openssl rand -hex 32)

Never reuse an example key from a public repo — every token "encrypted" with a published key is effectively stored in plaintext. Once set:

  1. Admin opens /settings/integrations/tracker, selects Huly, fills in instance URL / workspace / access token / project key.
  2. Test connection (POST /api/settings/tracker/test) tries the values on-screen without saving them — a blank form instead re-tests and refreshes the last-test status of whatever's already saved (used for the "test again" round trip once a token is on file and can no longer be shown back).
  3. Save (PUT /api/settings/tracker) runs the same SSRF check the adapter itself runs at file time — so a blocked destination is rejected at save, not silently accepted and only discovered the first time someone tries to file — then encrypts the token with AES-256-GCM under TRACKER_CREDENTIALS_KEY and writes the tracker_config singleton row. The token is never returned by a later GET.

Every configure/test action is audit-logged with actor, tracker kind and outcome — never the raw token.

Security: the hardened seam

All six adapters share one hardened core (src/lib/qa/trackers/http.ts, src/lib/http/ssrf.ts):

  • SSRF guard — before any outbound request, the destination URL is checked and rejected if it's a non-http(s) scheme, carries embedded credentials, or resolves to a private/internal host: loopback, 10/8, 172.16/12, 192.168/16, link-local 169.254/16, CGNAT 100.64/10, multicast/reserved, ::1, fc00::/7, fe80::/10, localhost and *.local / *.localhost / *.internal / *.localdomain hostnames. Since Teasynaer is self-hosted and your tracker is often on your own network, this is an opt-out, not a hard block:

    QA_ALLOW_PRIVATE_TRACKER_URL=true
    

    This is a literal-host check, not a DNS-rebinding-proof one — a public hostname that resolves to a private address at connect time isn't caught. Documented, not hidden.

  • Shared timeout + bounded retries across every adapter:

    QA_TRACKER_TIMEOUT_MS=10000    # per-request timeout
    QA_TRACKER_RETRIES=2           # bounded retries, 5xx/429 only
    
  • Typed error taxonomy — network failure, HTTP failure, invalid response — so a failed filing attempt surfaces a generic, safe message (502 with e.g. "Failed to reach GitHub.") rather than a raw exception, a token, or a URL.

  • Secrets never logged. A failed fileIssue() call logs the HTTP status and a generic message only.

  • Credentials encrypted at rest. Applies to the one adapter that's DB-backed today (Huly) — AES-256-GCM under TRACKER_CREDENTIALS_KEY. The env-configured adapters' tokens live wherever your deployment stores its environment (Docker secrets, your platform's secret store, etc.) — Teasynaer never persists them itself.

No tracker, or filing failed: the export packet

The "Export packet" button on the On-Fail screen is always there, tracker or no tracker — it's the manual handoff path for dogfooding, for teams still deciding which tracker to wire up, and for the moment a filing attempt returns an error.

  • If no tracker is configured, "File issue" returns 409 ({"error": "tracker_not_configured", "setupUrl": "/settings/integrations/tracker"}). An admin gets routed straight into setup; a member sees "Ask an admin or export the packet" and the Export packet button right beside it.
  • If a tracker is configured but the call fails (network, auth, rate limit), filing returns 502 with a generic message; your notes and evidence are already saved, so nothing is lost — try "Export packet" instead, or fix the tracker and retry filing.
curl -H "Cookie: $SESSION_COOKIE" \
  https://your-teasynaer.example/api/qa/runs/$RUN_ID/results/$RESULT_ID/export-packet

Downloads a teasynaer-packet-<case-key>.json containing a manifest (title, notes, case detail, evidence summary, per-file download links) and a ready-to-paste markdown string — the same content an adapter would have filed, minus the adapter.

Notifications (the Notifier seam)

A separate, smaller seam tells a chat channel that something happened — a run completed, or an issue got filed. It is not a tracker: it never files an issue and never records a verdict, and a delivery failure never blocks the run or the filing that triggered it.

This is an environment-variable-only MVP — there's no in-app config UI, no in-app bell, and no email digest yet (that's the follow-up scope; see below). Every sink is independently gated: leave its URL unset and that sink is simply off.

# Slack Incoming Webhook (posts { text, blocks }):
QA_NOTIFY_SLACK_URL=

# Microsoft Teams Incoming Webhook (posts a MessageCard):
QA_NOTIFY_TEAMS_URL=

# Generic webhook — posts the raw event as JSON, expects no response body:
QA_NOTIFY_WEBHOOK_URL=
QA_NOTIFY_WEBHOOK_TOKEN=        # optional; sent as "Authorization: Bearer <token>"

# Which events fire — comma list of "run.completed" and/or "issue.filed".
# Unset/blank enables both; the real on/off switch is whether any URL above is set.
QA_NOTIFY_EVENTS=run.completed,issue.filed

# Same opt-out as the tracker guard, for a notify URL on your own network:
QA_ALLOW_PRIVATE_NOTIFY_URL=false

An issue.filed notification carries the failing case's title and a link back into Teasynaer only — never the reproduction notes or evidence content. That's a deliberately tighter bar than what goes into a filed issue: a chat channel isn't a system your org has already decided to trust with the full case detail.

Every send is fire-and-forget: notifications reuse the same SSRF guard and timeout/retry policy as the tracker adapters, and a sink that's misconfigured or unreachable is logged (never with its URL or token) and simply skipped — it never surfaces to the tester and never delays the response.

Not built yet

  • A DB-backed config UI mirroring the tracker wizard.
  • An in-app notification bell.
  • An email digest.

All of the above are follow-up scope on the same seam, not silently-dropped features — the MVP here is the outbound webhook half only.

Troubleshooting

SymptomLikely cause
"File issue" returns tracker_not_configuredNothing is saved in the DB and no adapter's env vars are fully set. Check GET /api/settings/tracker, or just open the wizard.
Saving Huly config fails with a key errorTRACKER_CREDENTIALS_KEY isn't set (or isn't set in the environment the app process actually reads).
Filing fails with "not an allowed destination"The tracker/bridge URL resolved to a private/internal host and QA_ALLOW_PRIVATE_TRACKER_URL (or QA_ALLOW_PRIVATE_NOTIFY_URL for a notify sink) isn't set.
QA_TRACKER=forgejo errors on boot / saveMigration 0014_forgejo_tracker.sql hasn't run yet — the tracker_kind enum doesn't have forgejo. Run migrations, then retry.
Labels don't show up on a filed Forgejo issueExpected — v1 doesn't resolve label names to IDs yet (see Forgejo / Gitea).
A Slack/Teams message never arrivesConfirm the sink's URL is set and that QA_NOTIFY_EVENTS (if set) includes the event you expected. Failures are logged server-side without the URL — check the app log, not the chat channel.

Post-MVP backlog

The five env-configured adapters are shipped and working; the items below are UX polish layered on top, plus a couple of additional destinations:

AreaWhat's missing today
GitHub / GitLab / Jira / Webhook / ForgejoIn-app credential entry (currently env-only), native attachment upload
GitHubOptional OAuth instead of a pasted PAT
Additional destinationsLinear
Tracker routingPer-team / per-package tracker override (today: one tracker per instance)
NotifierDB-backed config UI, in-app bell, email digest

See also

← All docs