> Documentation index: [Saleor](/llms.txt) · [This section](/developer/llms.txt)
> Source: https://docs.saleor.io/developer/extending/apps/developing-apps/apps-patterns/reporting-problems

# 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+.

<a id="overview"></a>

## 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.

<a id="creating-problems"></a>

## Creating problems

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

```graphql
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
    }
  }
}
```

<a id="input-fields"></a>

### 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. |

<a id="choosing-a-key"></a>

### 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`

<a id="aggregation"></a>

## 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.

```text
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.

<a id="critical-thresholds"></a>

## Critical thresholds

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

<a id="immediate-critical"></a>

### Immediate critical

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

```graphql
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`.

<a id="gradual-escalation"></a>

### Gradual escalation

Set a higher threshold to escalate only after repeated occurrences:

```graphql
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`.

<a id="de-escalation"></a>

### 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

<a id="dismissing-problems"></a>

## 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.

<a id="as-an-app"></a>

### 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:**

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

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

```graphql
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.

<a id="as-a-staff-user"></a>

### 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):

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

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

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

<a id="caller-restrictions"></a>

### 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.

<a id="querying-problems"></a>

## 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):

```graphql
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).

<a id="appproblem-fields"></a>

### 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). |

<a id="appproblemdismissed-fields"></a>

### 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`. |

<a id="clearing-problems-on-recovery"></a>

## 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:

```graphql
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.

<a id="business-examples"></a>

## Business examples

<a id="payment-app-gateway-connectivity"></a>

### 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.

```graphql
# 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
    }
  }
}
```

<a id="fulfillment-app-missing-api-credentials"></a>

### 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.

```graphql
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.

<a id="tax-app-degraded-external-service"></a>

### 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.

```graphql
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.

<a id="shipping-app-rate-limit-exceeded"></a>

### 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.

```graphql
# 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.

<a id="limits-and-eviction"></a>

## 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.

<a id="permissions"></a>

## 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. |
