Skip to main content

Reporting Problems from Apps

Apps can self-report operational problems visible in the Saleor Dashboard. This lets you surface issues — such as misconfiguration, missing credentials, or degraded external services — directly to store administrators without requiring them to inspect logs or external monitoring.

info

This feature is available in Saleor 3.22+.

Overview

The Problems API provides two mutations:

  • appProblemCreate — report a problem with a key and message.
  • appProblemDismiss — dismiss problems by IDs or keys.

Problems are visible on the App type via the problems field and are displayed in the Dashboard on the app detail page.

Key behaviors:

  • Problems with the same key are aggregated within a configurable time window (default: 60 minutes).
  • A criticalThreshold can mark a problem as critical once occurrences reach a given count.
  • Each app can have up to 100 problems. When the limit is reached, the oldest problem is evicted automatically.
  • Problems are cascade-deleted when the app is removed.

Creating problems

Use appProblemCreate to report a problem. This mutation is only available to authenticated apps — staff users cannot call it.

mutation {
appProblemCreate(
input: {
message: "Cannot connect to payment gateway: connection timeout"
key: "payment-gateway-health"
}
) {
appProblem {
id
message
key
count
isCritical
}
errors {
field
message
code
}
}
}

Input fields

FieldTypeRequiredDefaultDescription
messageString!YesHuman-readable description of the problem. Must be at least 3 characters. Messages longer than 2048 characters are truncated.
keyString!YesIdentifier for the type of problem. Must be between 3 and 128 characters. Used for aggregation and dismissal by key.
aggregationPeriodMinuteNo60Time window in minutes for aggregating problems with the same key. Set to 0 to disable aggregation.
criticalThresholdPositiveIntNoWhen set, the problem is marked as isCritical: true once its count reaches this value.

Choosing a key

The key field identifies the category of problem. Use a descriptive, stable identifier that represents the type of issue rather than a specific occurrence. The same key is used for:

  • Aggregation — repeated calls with the same key within the aggregation window are merged into a single problem with an incrementing count.
  • Dismissal by key — you can dismiss all problems matching a key in one call.

Good key examples:

  • payment-gateway-health
  • warehouse-api-auth
  • shipping-provider-timeout
  • tax-service-config-missing

Aggregation

When you report a problem with the same key as an existing non-dismissed problem, the system checks if the last event (updatedAt) is within the aggregationPeriod. If it is, the problems are merged:

  • count is incremented.
  • message is updated to the new message.
  • updatedAt is reset — restarting the aggregation window.

This is a sliding window measured from the last event, not from the first. As long as events keep arriving within the window, they aggregate into a single problem.

T+0min   appProblemCreate(key: "api-err") → new problem (count=1)
T+30min appProblemCreate(key: "api-err") → merged (count=2, window resets)
T+80min appProblemCreate(key: "api-err") → merged (count=3, 50 min since last — still within 60 min)
T+200min appProblemCreate(key: "api-err") → new problem (count=1, 120 min since last — exceeded window)

Setting aggregationPeriod: 0 disables aggregation — every call creates a new problem.

note

Dismissed problems are excluded from aggregation. If a problem with a given key was dismissed, a new report with the same key creates a fresh problem.

Critical thresholds

Use criticalThreshold to control when a problem is marked as critical. The problem's isCritical field becomes true once count >= criticalThreshold.

Immediate critical

Set criticalThreshold: 1 to mark a problem as critical from the first report:

mutation {
appProblemCreate(
input: {
message: "Payment provider certificate expired"
key: "payment-cert-expired"
criticalThreshold: 1
}
) {
appProblem {
key
count
isCritical
}
errors {
field
message
code
}
}
}

Result: count: 1, isCritical: true.

Gradual escalation

Set a higher threshold to escalate only after repeated occurrences:

mutation {
appProblemCreate(
input: {
message: "Warehouse API returns 401"
key: "warehouse-api-health"
aggregationPeriod: 60
criticalThreshold: 5
}
) {
appProblem {
key
count
isCritical
}
errors {
field
message
code
}
}
}

After 4 calls: count: 4, isCritical: false. After the 5th call: count: 5, isCritical: true.

De-escalation

If a subsequent call sends a higher criticalThreshold than the current count, the problem is de-escalated back to non-critical:

  • After 5 calls with criticalThreshold: 5: count: 5, isCritical: true
  • 6th call with criticalThreshold: 10: count: 6, isCritical: false — threshold raised above current count

Dismissing problems

Both apps and staff users can dismiss problems. The appProblemDismiss mutation uses a structured input — exactly one of byApp, byStaffWithIds, or byStaffWithKeys must be provided.

As an app

App callers use the byApp input to dismiss their own problems. Exactly one of ids or keys must be provided inside byApp.

Dismiss by IDs:

mutation {
appProblemDismiss(
input: {
byApp: {
ids: ["QXBwUHJvYmxlbTox"]
}
}
) {
errors {
field
message
code
}
}
}

Dismiss by keys — dismiss all problems matching the given keys:

mutation {
appProblemDismiss(
input: {
byApp: {
keys: ["warehouse-api-health"]
}
}
) {
errors {
field
message
code
}
}
}
caution

Inside byApp, ids and keys cannot be used together. Exactly one of them is required.

As a staff user

Staff users with MANAGE_APPS permission can dismiss problems using byStaffWithIds or byStaffWithKeys.

Dismiss by IDs (app is inferred from the problem IDs):

mutation {
appProblemDismiss(
input: {
byStaffWithIds: {
ids: ["QXBwUHJvYmxlbTox"]
}
}
) {
errors {
field
message
code
}
}
}

Dismiss by keys (requires app argument to identify the target app):

mutation {
appProblemDismiss(
input: {
byStaffWithKeys: {
keys: ["warehouse-api-health"]
app: "QXBwOjE="
}
}
) {
errors {
field
message
code
}
}
}

Caller restrictions

InputWho can use itidskeysapp field
byAppApp onlyDismiss own problems by IDDismiss own problems by keyNot applicable
byStaffWithIdsStaff onlyDismiss by ID (app is inferred)Not applicable
byStaffWithKeysStaff onlyDismiss by key for a given appRequired

Staff users need the MANAGE_APPS permission and can only dismiss problems for apps within their permission scope. Each list (ids or keys) is limited to 100 items per call.

Querying problems

Problems are available on the App type. You can query them through any query that returns an App. An optional limit parameter restricts the number of returned problems (must be between 1 and 100):

query {
app(id: "QXBwOjE=") {
id
name
problems(limit: 10) {
id
message
key
createdAt
updatedAt
count
isCritical
dismissed {
by
user {
id
email
}
userEmail
}
}
}
}

Problems are ordered by createdAt descending (newest first).

AppProblem fields

FieldTypeDescription
idID!Unique identifier.
messageString!Human-readable problem description. Updated on aggregation.
keyString!Identifier for the type of problem.
countInt!Number of occurrences (incremented on aggregation).
isCriticalBoolean!Whether the problem reached the critical threshold.
dismissedAppProblemDismissedDismissal information. null if the problem has not been dismissed.
createdAtDateTime!When the problem was first created.
updatedAtDateTime!When the problem was last updated (reset on each aggregation).

AppProblemDismissed fields

The dismissed field returns an object with dismissal details when the problem has been dismissed, or null otherwise.

FieldTypeDescription
byAppProblemDismissedByEnum!Whether dismissed by APP or USER.
userUserThe user who dismissed the problem. null if dismissed by an app or if the user was deleted. Requires MANAGE_STAFF permission.
userEmailStringEmail of the user who dismissed the problem. Preserved even if the user is deleted. Requires AUTHENTICATED_STAFF_USER.

Clearing problems on recovery

When the underlying issue is resolved, your app should dismiss the related problems so the Dashboard reflects the current state. Use appProblemDismiss with the byApp input and keys argument to clear all problems of a given type:

mutation {
appProblemDismiss(
input: {
byApp: {
keys: ["payment-gateway-health"]
}
}
) {
errors {
field
message
code
}
}
}

This pattern works well with health-check loops: report a problem on failure, dismiss it once the service recovers.

Business examples

Payment app: gateway connectivity

A payment app monitors the health of its gateway connection. On each failed health check, it reports a problem. After 5 consecutive failures within the aggregation window, the problem becomes critical.

# On each failed health check:
mutation {
appProblemCreate(
input: {
message: "Payment gateway unreachable: connection timeout after 5s"
key: "payment-gateway-connectivity"
aggregationPeriod: 30
criticalThreshold: 5
}
) {
appProblem {
key
count
isCritical
}
errors {
field
message
code
}
}
}

# When the gateway recovers:
mutation {
appProblemDismiss(
input: {
byApp: {
keys: ["payment-gateway-connectivity"]
}
}
) {
errors {
field
message
code
}
}
}

Fulfillment app: missing API credentials

A fulfillment app detects that the warehouse API key is missing or invalid during startup. This is a configuration issue that requires immediate attention.

mutation {
appProblemCreate(
input: {
message: "Warehouse API key is missing. Configure it in the app settings."
key: "warehouse-api-credentials"
criticalThreshold: 1
}
) {
appProblem {
key
count
isCritical
}
errors {
field
message
code
}
}
}

Result: isCritical: true immediately, prompting the store administrator to act.

Tax app: degraded external service

A tax calculation app gets intermittent errors from the tax provider. It reports each failure but only escalates to critical after 10 occurrences, since occasional failures are expected.

mutation {
appProblemCreate(
input: {
message: "Tax provider returned HTTP 503: service temporarily unavailable"
key: "tax-provider-availability"
aggregationPeriod: 120
criticalThreshold: 10
}
) {
appProblem {
key
count
isCritical
}
errors {
field
message
code
}
}
}

With aggregationPeriod: 120, failures within a 2-hour sliding window accumulate. If the provider returns 10 errors in that window, the problem becomes critical.

Shipping app: rate-limit exceeded

A shipping rate app detects that it has been rate-limited by the carrier API. It reports the problem and clears it once the rate limit resets.

# When rate-limited:
mutation {
appProblemCreate(
input: {
message: "Carrier API rate limit exceeded. Shipping rates may be unavailable until the limit resets."
key: "carrier-rate-limit"
criticalThreshold: 1
aggregationPeriod: 0
}
) {
appProblem {
key
count
isCritical
}
errors {
field
message
code
}
}
}

With aggregationPeriod: 0, each rate-limit event creates a separate problem record — useful when you want a log of individual incidents.

Limits and eviction

Each app can store up to 100 problems. When the limit is reached, creating a new problem automatically evicts the oldest one. This means you do not need to worry about cleanup — old problems are removed to make room for new ones.

If you want finer control, dismiss problems proactively using appProblemDismiss when the underlying issue is resolved.

Permissions

OperationRequired permissionNotes
appProblemCreateAUTHENTICATED_APPOnly the app itself can create its own problems.
appProblemDismissAUTHENTICATED_APP or MANAGE_APPSApps dismiss their own. Staff can dismiss for managed apps.
Querying problemsAUTHENTICATED_APP or MANAGE_APPSAvailable on the App type with appropriate permissions.
dismissed.userMANAGE_STAFFViewing which user dismissed a problem requires staff permission.
dismissed.userEmailAUTHENTICATED_STAFF_USEROnly accessible to authenticated staff users; apps receive a permission error.