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 IDIssues 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_..., oriss_...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:) { }.
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:) { }.
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.
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 { }.
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.
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:
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.
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

