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.
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
keyare aggregated within a configurable time window (default: 60 minutes). - A
criticalThresholdcan 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
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
message | String! | Yes | — | Human-readable description of the problem. Must be at least 3 characters. Messages longer than 2048 characters are truncated. |
key | String! | Yes | — | Identifier for the type of problem. Must be between 3 and 128 characters. Used for aggregation and dismissal by key. |
aggregationPeriod | Minute | No | 60 | Time window in minutes for aggregating problems with the same key. Set to 0 to disable aggregation. |
criticalThreshold | PositiveInt | No | — | When 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-healthwarehouse-api-authshipping-provider-timeouttax-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:
countis incremented.messageis updated to the new message.updatedAtis 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.
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
}
}
}
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
| Input | Who can use it | ids | keys | app field |
|---|---|---|---|---|
byApp | App only | Dismiss own problems by ID | Dismiss own problems by key | Not applicable |
byStaffWithIds | Staff only | Dismiss by ID (app is inferred) | — | Not applicable |
byStaffWithKeys | Staff only | — | Dismiss by key for a given app | Required |
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
| Field | Type | Description |
|---|---|---|
id | ID! | Unique identifier. |
message | String! | Human-readable problem description. Updated on aggregation. |
key | String! | Identifier for the type of problem. |
count | Int! | Number of occurrences (incremented on aggregation). |
isCritical | Boolean! | Whether the problem reached the critical threshold. |
dismissed | AppProblemDismissed | Dismissal information. null if the problem has not been dismissed. |
createdAt | DateTime! | When the problem was first created. |
updatedAt | DateTime! | 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.
| Field | Type | Description |
|---|---|---|
by | AppProblemDismissedByEnum! | Whether dismissed by APP or USER. |
user | User | The user who dismissed the problem. null if dismissed by an app or if the user was deleted. Requires MANAGE_STAFF permission. |
userEmail | String | Email 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
| Operation | Required permission | Notes |
|---|---|---|
appProblemCreate | AUTHENTICATED_APP | Only the app itself can create its own problems. |
appProblemDismiss | AUTHENTICATED_APP or MANAGE_APPS | Apps dismiss their own. Staff can dismiss for managed apps. |
Querying problems | AUTHENTICATED_APP or MANAGE_APPS | Available on the App type with appropriate permissions. |
dismissed.user | MANAGE_STAFF | Viewing which user dismissed a problem requires staff permission. |
dismissed.userEmail | AUTHENTICATED_STAFF_USER | Only accessible to authenticated staff users; apps receive a permission error. |