/ Use case

GraphQL API Monitoring

A 200 with a populated errors array is still an outage

GraphQL API Monitoring

GraphQL answers 200 whether the query worked or not. The failure sits in the errors array in the body, where a status-code monitor never looks. Assert sends the query, reads the body, and fails the check on what it finds there.

The Problem

Five ways a GraphQL endpoint fails while still reporting success:

  • 200 with errors: the server reports success and populates errors
  • Partial responses: some fields resolve, the rest come back null
  • N+1 queries: the resolver gets slower every week and never errors
  • Schema changes: a removed field breaks clients while the status code holds at 200
  • Resolver failures: one field fails on its own and nothing downstream notices

How Assert Helps

Execute Real Queries

Assert sends the query itself, with its variables:

Monitor: User Profile Query
Query:
  query GetUser($id: ID!) {
    user(id: $id) {
      id
      email
      profile {
        name
        avatar
      }
    }
  }
Variables: {"id": "user_123"}

Detect GraphQL Errors

The one assertion every GraphQL monitor needs is that errors never shows up:

Assertions:
  ✓ $.errors does not exist
  ✓ $.data.user exists
  ✓ $.data.user.email contains "@"

Monitor Mutations

Mutations get the same treatment. Run one against a test account and check what comes back:

Monitor: Create Post Mutation
Query:
  mutation CreatePost($input: CreatePostInput!) {
    createPost(input: $input) {
      id
      title
      status
    }
  }
Variables: {"input": {"title": "Test", "content": "..."}}
Assertions:
  ✓ $.errors does not exist
  ✓ $.data.createPost.id exists
  ✓ $.data.createPost.status == "draft"

GraphQL-Specific Assertions

No Errors

This response is a 200:

// Bad response (200 OK but has errors):
{
  "data": null,
  "errors": [
    {
      "message": "User not found",
      "path": ["user"]
    }
  ]
}

Assertion: $.errors must not exist

Data Structure

Validate the expected data structure:

// Expected response:
{
  "data": {
    "users": [
      {"id": "1", "name": "Alice"},
      {"id": "2", "name": "Bob"}
    ]
  }
}

Assertions:

  • $.data.users must be array
  • $.data.users.length must be > 0
  • $.data.users[0].id must exist

Partial Failures

Some fields might fail while others succeed:

// Partial failure:
{
  "data": {
    "user": {
      "id": "1",
      "posts": null  // Failed to resolve
    }
  },
  "errors": [
    {"message": "Failed to fetch posts", "path": ["user", "posts"]}
  ]
}

Assertions:

  • $.data.user.posts must exist (catches null)
  • $.errors must not exist

Real-World Examples

Monitor a Public GraphQL API

Monitor: GitHub GraphQL API
URL: https://api.github.com/graphql
Method: POST
Headers:
  Authorization: Bearer $GITHUB_TOKEN
Body:
  query: |
    query {
      viewer {
        login
        repositories(first: 5) {
          nodes {
            name
          }
        }
      }
    }
Assertions:
  ✓ $.errors does not exist
  ✓ $.data.viewer.login exists
  ✓ $.data.viewer.repositories.nodes.length > 0
  ✓ Response time < 2000ms

Monitor an E-commerce GraphQL API

Monitor: Product Catalog Query
Query:
  query Products($first: Int!) {
    products(first: $first) {
      edges {
        node {
          id
          name
          price
          inventory {
            quantity
          }
        }
      }
    }
  }
Variables: {"first": 10}
Assertions:
  ✓ $.errors does not exist
  ✓ $.data.products.edges.length > 0
  ✓ $.data.products.edges[0].node.price > 0
  ✓ $.data.products.edges[0].node.inventory.quantity >= 0

Monitor Authentication

Monitor: Auth Query
Query:
  query Me {
    me {
      id
      email
      role
      permissions
    }
  }
Headers:
  Authorization: Bearer $TEST_USER_TOKEN
Assertions:
  ✓ $.errors does not exist
  ✓ $.data.me.id exists
  ✓ $.data.me.role in ["user", "admin"]

Common GraphQL Monitoring Patterns

Schema Health Check

Query introspection to verify schema is available:

query IntrospectionQuery {
  __schema {
    types {
      name
    }
  }
}

Assertions:

  • $.data.__schema.types.length > 0

Resolver Performance

Point a monitor at the resolvers you already know are slow:

query SlowResolver {
  analytics {
    dailyStats(days: 7) {
      date
      value
    }
  }
}

Assertions:

  • Response time < 3000ms
  • $.data.analytics.dailyStats.length == 7

Subscription Health (via HTTP)

If you expose subscription health:

GET /graphql/subscriptions/health
Assertions:
  ✓ $.connected == true
  ✓ $.activeSubscriptions >= 0

Best Practices

Monitor Critical Paths

Focus on queries that impact users:

Priority Query Type Example
Critical Authentication me, currentUser
Critical Core data products, orders
High Search searchProducts
Medium Analytics dashboardStats

Use Realistic Variables

Test with production-like data:

// Good: Realistic variables
{"userId": "user_abc123", "limit": 10}

// Bad: Minimal testing
{"userId": "1", "limit": 1}

Monitor from Multiple Regions

A query's latency depends on CDN caching, database proximity and resolver complexity, and all three change by region. Assert runs each check from six regions and wants them to agree before it opens an incident.

Test Error Handling

Point a monitor at a request that should fail, and assert on the error you expect back:

Monitor: Invalid ID Handling
Query: user(id: "invalid_id") { id }
Assertions:
  ✓ Status code == 200
  ✓ $.errors[0].message contains "not found"

If someone changes that message, this check finds out before a client does.

Track Response Times

A dashboard query and a me query have nothing in common, so give them separate budgets:

Assertions:
  ✓ Response time < 500ms  (simple queries)
  ✓ Response time < 2000ms (complex queries)
  ✓ Response time < 5000ms (analytics queries)

Alert Configuration

However you route them, Assert sends one alert per incident with the failing response attached, and closes it on its own when the next check passes.

Critical

Condition: $.errors exists on core queries
Action: Page on-call
Reason: Users can't access data

Performance

Condition: Response time > 3000ms
Action: Slack alert
Reason: User experience degrading

Schema Issues

Condition: Introspection query fails
Action: Email engineering
Reason: Schema may be broken

Getting Started

  1. List the queries users hit first, starting with authentication and core data
  2. Build one monitor per query, with realistic variables
  3. Assert $.errors does not exist on every one of them
  4. Assert on the shape of $.data underneath it
  5. Give each query a response time budget and assert on that too

Related Features

Write your first assertion.

Five monitors, thirty-second checks, no card, no call.