
# GraphQL Analytics Policy

The GraphQL Analytics policy makes failed GraphQL operations visible on Zuplo's
GraphQL analytics dashboard. GraphQL servers following the standard Apollo /
graphql-yoga pattern return `200 OK` with an `errors[]` array in the response
body when an operation fails — invisible to HTTP-level analytics, which would
report every such operation as a success.

Add this policy to a GraphQL route (marked `x-graphql: true`) and the gateway
reads the response body, counts the GraphQL errors, and classifies each one by
its `extensions.code` (syntax, validation, auth, timeout, or resolver) — no
changes to your GraphQL server or client code required. The classification map
is configurable for servers that emit custom error codes, and errors can
optionally be written to the request log.

:::caution{title="Beta"}

This policy is in beta. You can use it today, but it may change in non-backward compatible ways before the final release.

:::

## Configuration

The configuration shows how to configure the policy in the 'policies.json' document.

```json title="config/policies.json"
{
  "name": "my-graphql-analytics-outbound-policy",
  "policyType": "graphql-analytics-outbound",
  "handler": {
    "export": "GraphqlAnalyticsOutboundPolicy",
    "module": "$import(@zuplo/runtime)",
    "options": {
      "errorCodeClassification": {
        "RATE_LIMITED": "resolver",
        "NOT_LOGGED_IN": "auth"
      },
      "defaultErrorClass": "resolver",
      "logErrors": false
    }
  }
}
```

### Policy Configuration

- `name` <code className="text-green-600">&lt;string&gt;</code> - The name of your policy instance. This is used as a reference in your routes.
- `policyType` <code className="text-green-600">&lt;string&gt;</code> - The identifier of the policy. This is used by the Zuplo UI. Value should be `graphql-analytics-outbound`.
- `handler.export` <code className="text-green-600">&lt;string&gt;</code> - The name of the exported type. Value should be `GraphqlAnalyticsOutboundPolicy`.
- `handler.module` <code className="text-green-600">&lt;string&gt;</code> - The module containing the policy. Value should be `$import(@zuplo/runtime)`.
- `handler.options` <code className="text-green-600">&lt;object&gt;</code> - The options for this policy. [See Policy Options](#policy-options) below.

### Policy Options

The options for this policy are specified below. All properties are optional unless specifically marked as required.

- `errorCodeClassification` <code className="text-green-600">&lt;object&gt;</code> - Additional `extensions.code` → error-class mappings for codes your GraphQL server emits. Entries are merged over (and win against) the built-in Apollo-convention map (`GRAPHQL_PARSE_FAILED` → `syntax`, `GRAPHQL_VALIDATION_FAILED` / `BAD_USER_INPUT` → `validation`, `UNAUTHENTICATED` / `FORBIDDEN` → `auth`, timeout codes → `timeout`, `INTERNAL_SERVER_ERROR` → `resolver`). Keys are matched case-sensitively; built-in codes are matched case-insensitively.
- `defaultErrorClass` <code className="text-green-600">&lt;string&gt;</code> - The error class reported for a GraphQL error whose `extensions.code` is missing or not in the classification map. Allowed values are `syntax`, `validation`, `auth`, `timeout`, `resolver`. Defaults to `"resolver"`.
- `logErrors` <code className="text-green-600">&lt;boolean&gt;</code> - When `true`, also write a structured warning to the request log (message, `extensions.code`, and path of each error — capped at the first 10) whenever a response contains GraphQL errors. Defaults to `false`.
- `maxResponseBytes` <code className="text-green-600">&lt;integer&gt;</code> - Maximum response body size in bytes the policy will inspect. Larger bodies — by `Content-Length`, or measured while reading when the header is absent — pass through without being scanned, so their GraphQL errors (if any) go unreported. The default is 5 MiB. Defaults to `5242880`.

## Using the Policy

This policy reads GraphQL `errors[]` from response bodies and reports them to
Zuplo's GraphQL analytics, so operations that fail with the standard "`200 OK`
with errors" pattern (Apollo Server, graphql-yoga, and most other GraphQL
servers) show up as failures on the GraphQL dashboard instead of successes.

The response always passes through to the client unchanged — the body is read
from a clone, and any internal failure inside the policy is swallowed so error
reporting can never break a request.

## Requirements

The route must be marked with `"x-graphql": true` in `routes.oas.json`. That
marker is what enables GraphQL analytics for the route (one `graphql_operation`
event per request); this policy enriches that event with the errors it finds in
the response body. On a route without the marker the policy logs a warning and
does nothing.

## Error classification

Each entry in the response's `errors[]` array counts as one error toward the
operation's `errorCount`, and is classified into one of five classes from its
`extensions.code`, following the Apollo Server conventions:

| `extensions.code`                                                                                                                           | Class        |
| ------------------------------------------------------------------------------------------------------------------------------------------- | ------------ |
| `GRAPHQL_PARSE_FAILED`                                                                                                                      | `syntax`     |
| `GRAPHQL_VALIDATION_FAILED`, `BAD_USER_INPUT`, `PERSISTED_QUERY_NOT_FOUND`, `PERSISTED_QUERY_NOT_SUPPORTED`, `OPERATION_RESOLUTION_FAILURE` | `validation` |
| `UNAUTHENTICATED`, `FORBIDDEN`                                                                                                              | `auth`       |
| `REQUEST_TIMEOUT`, `TIMEOUT`, `GATEWAY_TIMEOUT`                                                                                             | `timeout`    |
| `INTERNAL_SERVER_ERROR`, `DOWNSTREAM_SERVICE_ERROR`                                                                                         | `resolver`   |

An error whose code is missing or unrecognized is classified as
`defaultErrorClass` (`resolver` unless configured otherwise). Servers that emit
their own codes can extend or override the table with `errorCodeClassification`;
those entries are matched case-sensitively and win against the built-ins, which
are matched case-insensitively.

Batched (array) responses are supported — errors are collected across every
result in the batch.

## What is inspected

Only responses with a JSON content type (`application/json` or any `+json` type
such as `application/graphql-response+json`) are read, and bodies larger than
`maxResponseBytes` (5 MiB by default) are skipped — by `Content-Length` when the
header is present, or measured while reading when it is absent. Everything else
passes through without the body being touched.

## Logging

Set `logErrors` to `true` to also write a structured warning to the request log
whenever a response contains GraphQL errors. The entry carries the message,
`extensions.code`, and path of each error (capped at the first 10) under a
consistent `"GraphQL response contained errors"` message, so you can search or
alert on it.

## Configuration

- `errorCodeClassification`: Additional `extensions.code` → class mappings,
  merged over the built-in table. **Default:** none
- `defaultErrorClass`: Class for errors with a missing or unrecognized code —
  one of `syntax`, `validation`, `auth`, `timeout`, `resolver`. **Default:**
  `resolver`
- `logErrors`: Also log a structured warning per errored response. **Default:**
  `false`
- `maxResponseBytes`: Maximum response body size in bytes to inspect.
  **Default:** `5242880` (5 MiB)

## Usage

Apply this policy to outbound responses on your GraphQL route:

```json
{
  "policies": [
    {
      "name": "graphql-analytics",
      "policyType": "graphql-analytics-outbound",
      "handler": {
        "export": "GraphqlAnalyticsOutboundPolicy",
        "module": "$import(@zuplo/runtime)",
        "options": {
          "errorCodeClassification": {
            "RATE_LIMITED": "resolver",
            "NOT_LOGGED_IN": "auth"
          },
          "logErrors": true
        }
      }
    }
  ]
}
```

Read more about [how policies work](/articles/policies)
