For the complete documentation index, see llms.txt. This page is also available as Markdown.

Issues API

Pull Duku findings into your tools and write triage decisions back.

Most teams use this to hand Duku's findings to whatever they already build with - a coding agent, a script, or their own issue tracker. Every entry says what is broken and carries the exact request, message, and step that produced it, so an agent can act on it without anyone re-describing the bug first.

All operations need a Bearer token. See API Keys.

Before you start

You need two things.

An API key - in the dashboard, go to Settings → API keys → + Generate key. It is shown once, so copy it straight away. A key can only see the organisation it was created in. See API Keys for how keys work.

Your product ID - open the product in the dashboard and take the sms_... value out of the URL:

https://app.duku.ai/p/sms_AbC123XyZ456
                      ^^^^^^^^^^^^^^^^
                      your product ID

Issues and signals

Duku reports what it finds at two levels.

  • An issue (isg_...) is one problem in your app, with a written explanation and a severity.

  • A signal (sig_..., or iss_... on older findings) is one distinct error behind it - a specific failed request, exception or console error. Every run that hits it adds an occurrence.

An issue groups one or more signals. See Issues & signals for how Duku groups errors.

Get everything for a product in one query

query AllIssues($subjectId: ID!) {
  issues(subjectId: $subjectId, status: OPEN, sortBy: PRIORITY, sortDirection: desc) {
    id
    title
    description
    bucket
    signals { id }
  }
  signalsConnection(subjectId: $subjectId, status: OPEN, first: 500, sortBy: PRIORITY, sortDirection: desc) {
    pageInfo { hasNextPage endCursor }
    edges { node {
      id
      issueId
      title
      occurrences(first: 1) {
        runId
        sampleError { message actionDescription }
      }
    } }
  }
}

subjectId is your product ID - the sms_... in the dashboard URL.

This is the smallest useful query. Every field in Fields you can add can go into it - add only the ones you need.

The response has two lists:

  • issues - the problems, one entry per underlying cause, each with a written explanation and the ids of the signals behind it.

  • signalsConnection - every signal, with the detail of what failed. "Connection" is the GraphQL name for a list you page through.

Match each signal's issueId to an issue's id (or an issue's signals[].id to a signal's id). Both lists are needed - an issue lists which signals belong to it, but the error detail lives on the signal. Products that are not grouped yet return issues as an empty array and the full picture in signalsConnection, so the same query works either way.

To run it, see Save it as JSON below - it covers exchanging your key for a token and posting the query.

Save it as JSON

Export your key, then save the query as query.json with your product ID as the variable:

Re-run it on a schedule, or after each deploy, to keep a current copy alongside your own issue data.

Fields you can add

Add any of these to the query above. Fields with { } take sub-fields - list the ones you want inside the braces.

On each issue

These go inside issues { } or issue(id:) { }.

Field
What it is

id

The issue's identifier (isg_...). Pass it to issue(id:) and setIssuePriority

title

A one-line statement of the problem

description

What a user does, what the app does wrong, and what to look at

status

OPEN, TRIAGED, RESOLVED or IGNORED - see Writing back status and priority

bucket

Severity - CRITICAL, HIGH, MEDIUM, LOW

priorityScore

A numeric priority that orders issues within the same severity

category

The kind of error, for example API_ERROR or JAVASCRIPT_ERROR

categories

Every kind of error among its signals

incident

true when Duku flagged it as an incident

signalCount

How many signals are behind it

occurrenceCount

How many distinct runs hit it - not how many errors

firstSeenAt / lastSeenAt

When it was first and last observed

occurrenceBuckets(buckets:, windowDays:)

Runs per time slice over a rolling window (14 days by default), oldest first - enough to draw a trend line

signals { id title errorType message url runId lastSeenAt }

A short summary of each signal behind it, most impactful first, including the latest error message and page

memberSignalIds

The ids of every signal behind it - pass them to the bulk mutations

representativeSignalId

The signal that best represents it

externalTicket { provider externalKey url remediationState createdAt }

The latest ticket filed for it in your tracker, for example Linear

filedAt

When it was first filed to your tracker

confirmedAt

When someone in your organisation confirmed it by filing it

credentialSetCounts { name occurrenceCount }

How many runs hit it per persona (credential set)

occurrences(first:) { ... }

One sample per affected run, newest first. Takes the same fields as a signal's occurrences below

With a status filter, signalCount, occurrenceCount and memberSignalIds count only the signals in that status. signals always lists every signal.

On each signal

These go inside signalsConnection { edges { node { } } } or signal(id:) { }.

Field
What it is

id

The signal's identifier (sig_..., or iss_... for older findings). Pass it to updateSignalStatus

issueId

The issue it belongs to - match it to an issue's id

title

A one-line label for this specific error

description

A longer explanation, when there is one

status

OPEN, TRIAGED, RESOLVED or IGNORED

category

The kind of error, for example API_ERROR or CONSOLE_ERROR

origin

CLIENT_APP for your own code, THIRD_PARTY for an external script, UNKNOWN if it could not be determined

priorityScore

A numeric priority

occurrenceCount

How many times this error was seen, across all runs

affectedBuildsCount

How many builds it was seen on

representativeUrl

The page or endpoint it is most associated with

firstSeenAt / lastSeenAt

When it was first and last observed

firstSeenTargetId / lastSeenTargetId

The build it was first and last seen on

occurrenceBuckets(buckets:, windowDays:)

Occurrences per time slice over a rolling window, oldest first

credentialSetCounts { name occurrenceCount }

How often it was seen per persona (credential set)

occurrences(first:) { ... }

The runs it was seen in, newest first - see the next table

On each occurrence

These go inside occurrences(first:) { }. first defaults to 100 and goes up to 1000; first: 1 returns the most recent.

Field
What it is

runId

The run it was captured in, for looking up in the dashboard

occurredAt

When it happened

credentialSetName

The persona (credential set) the run used

target { name version environment appVersion { label commitSha } }

The build and release it happened on

run { id name startTime }

The run it happened in

sampleError { ... }

The error itself - see the next table

On each sample error

These go inside sampleError { }.

Field
What it is

message

The verbatim error, including the stack for JavaScript errors

type

CONSOLE_ERROR, HTTP_CLIENT_ERROR, HTTP_SERVER_ERROR, JS_EXCEPTION, NETWORK_FAILURE, SCREENSHOT_REVIEW, RUN_REVIEW (visual issues)

actionDescription

The step that triggered it, for example Click on <button>: Delete

url

The page it happened on

requestMethod / requestUrl / statusCode

The failed request

sourceFile / lineNumber / columnNumber

Where a JavaScript error was thrown

stackTrace { callFrames { functionName url lineNumber columnNumber } }

The JavaScript call stack, frame by frame

level

The console level, for example error

Which fields are filled depends on the error type - an HTTP error carries the request but no source file, a console error the reverse - so expect nulls rather than treating them as missing data. The API returns the failed request's method, URL and status code, but not request or response bodies or headers.

occurrenceCount on an issue counts distinct runs, not errors. An issue with 9 signals can report 63 - that is 63 runs affected. type is a string, not a fixed set, so treat unfamiliar values as "something else" rather than failing on them.

Narrowing it down

Both lists take status and sort with sortBy (PRIORITY, LAST_SEEN, FIRST_SEEN, OCCURRENCES). Leave out status to get every status. Beyond that they differ:

List
Also filters on

issues

category, categories, targetId, credentialSetId

signalsConnection

search, url, origin, priorityBucket (CRITICAL, HIGH, MEDIUM, LOW, UNSCORED), credentialSetId

first defaults to 100 and is capped at 500 - ask for more and you silently get 500, so page with after set to pageInfo.endCursor rather than raising it. signalsConnection is forward-only: last and before are rejected. issues is not paged and always returns the full list.

To pull one issue on its own, pass its id:

To pull one signal, use signal(id:) with any of the signal fields above.

Writing back status and priority

Statuses are OPEN, TRIAGED, RESOLVED and IGNORED.

Status
Use it for

OPEN

Newly found, not yet triaged

TRIAGED

Acknowledged and prioritised

RESOLVED

Fix deployed, not expected to recur

IGNORED

Known, won't-fix, or noise you do not want to see

Set the status of an issue

An issue takes its status from its signals, so you change it by setting every signal behind it in one call. This is what the dashboard does when you change an issue's status.

Get the issue's signal ids:

Then set them all:

Pass the memberSignalIds from issue(id:), or from an issues query with no status or category filter. With either filter, memberSignalIds holds only the matching signals, so the rest keep their status and the issue may not move.

An issue shows the first of these statuses that any of its signals has: OPEN, then TRIAGED, then RESOLVED, then IGNORED. It stays OPEN while any of its signals is OPEN, and reads IGNORED only when every signal is ignored.

Set the status of one signal

Set priority

Set an issue's priority with setIssuePriority(id: <issue id>, input: { priority: HIGH }). A priority you set sticks - Duku does not re-rank that issue afterwards. To set one signal's priority use setSignalPriorityOverride(id:, input: { priority: HIGH }), or bulkSetSignalPriorityOverride(input: { ids, priority }) for several.

Status and priority writes need only the Member role, so an ordinary API key can triage from a pipeline.

Last updated