# Debugging and Inspection

Debug `@data-client/vue` state and actions programmatically from any browser tool that runs JavaScript in the page. In dev mode, the app's [DevToolsManager](https://dataclient.io/vue/api/DevToolsManager.md#controllers) puts the store on `globalThis.__DC__`: `__DC__.controller` is its [Controller](https://dataclient.io/vue/api/Controller.md), so every Controller method works, and `__DC__.actions` is its recent [actions](https://dataclient.io/vue/api/Actions.md).

## Prerequisites

1. Dev server running with `NODE_ENV !== 'production'`
2. A browser tool that runs JavaScript in the page, with the app loaded (see Browser Tools)
3. `DevToolsManager` included in `DataClientPlugin` managers (default in dev mode)

## Browser Tools

Every snippet below is a function. Find your tool in the table: it runs snippets one of the ways below, and its
setup page says how to open the page.

| Tool                 | Runs a snippet with                           | How                                       | Network log                                            | Console                                               | Setup                                                                                                                                                                 |
| -------------------- | --------------------------------------------- | ----------------------------------------- | ------------------------------------------------------ | ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Cursor desktop agent | `browser_cdp`                                 | [Awaited expression](#awaited-expression) | [requests snippet](#without-a-network-or-console-tool) | [console capture](#without-a-network-or-console-tool) | [browser-cursor](https://raw.githubusercontent.com/reactive/data-client/master/.agents/skills/data-client-vue/references/browser-cursor.md#desktop-agent)             |
| Cursor cloud agent   | `browser_evaluate` once Playwright is enabled | [Function](#function)                     | `browser_network_requests`                             | `browser_console_messages`                            | [browser-cursor](https://raw.githubusercontent.com/reactive/data-client/master/.agents/skills/data-client-vue/references/browser-cursor.md#cloud-agent)               |
| Chrome DevTools MCP  | `evaluate_script`                             | [Function](#function)                     | `list_network_requests`                                | `list_console_messages`                               | [browser-chrome-devtools-mcp](https://raw.githubusercontent.com/reactive/data-client/master/.agents/skills/data-client-vue/references/browser-chrome-devtools-mcp.md) |
| Playwright MCP       | `browser_evaluate`                            | [Function](#function)                     | `browser_network_requests`                             | `browser_console_messages`                            | [browser-playwright-mcp](https://raw.githubusercontent.com/reactive/data-client/master/.agents/skills/data-client-vue/references/browser-playwright-mcp.md)           |
| Claude in Chrome     | `javascript_tool`                             | [Expression](#expression)                 | `read_network_requests`                                | `read_console_messages`                               | [browser-claude-in-chrome](https://raw.githubusercontent.com/reactive/data-client/master/.agents/skills/data-client-vue/references/browser-claude-in-chrome.md)       |

Any other tool works if it runs JavaScript in the page and returns the result: pick the way below that matches it
(Playwright's `page.evaluate` is a function; CDP `Runtime.evaluate` is an awaited expression). A browser that can
only click, type and read the console can't run these snippets; connect one of the tools above instead.

### Function

The tool takes a function, calls it in the page and awaits its result. Pass the snippet exactly as written.

### Awaited expression

The tool evaluates an expression, like CDP `Runtime.evaluate`. Call the snippet and have the tool await it:

- `expression`: `(<snippet>)()`
- `awaitPromise`: `true`
- `returnByValue`: `true`

The snippet's value is `result.value`. Check `exceptionDetails` first: a throw or a rejected promise lands there,
and a rejection still returns `result.value` as `{}`. `{}` is also what comes back for a bare function
(`result.type` `"function"`), an async snippet without `awaitPromise`, and a DOM node, so return plain data.

### Expression

The tool returns the value of the last expression and doesn't await promises. Call a sync snippet:
`(<snippet>)()`. Start an async one with this, then read `__DC_RESULT__` in the next call until it is set. It
holds `{ value }`, or `{ error }` if the snippet threw. Run one async snippet at a time.

```js
globalThis.__DC_RESULT__ = undefined; // so an earlier snippet's result can't be read as this one's
(<snippet>)().then(
  value => (globalThis.__DC_RESULT__ = { value }),
  e => (globalThis.__DC_RESULT__ = { error: String(e) }),
);
```

### Without a network or console tool

Request URLs, durations and statuses from the page's `performance` entries:

```js
// run in page — requests
() => performance.getEntriesByType('resource').map(e => ({
  url: e.name,
  duration: e.duration,
  status: e.responseStatus,
  transferSize: e.transferSize,
  bodySize: e.decodedBodySize,
}))
```

`transferSize` `0` with a nonzero `bodySize` means the browser's HTTP cache answered without asking the server; both are `0` for a cross-origin response without `Timing-Allow-Origin`, which hides sizes. `status` is `0` for any cross-origin response read without CORS (images and fonts too, even when they load), and
blocked requests can be missing, so check failures in the key's `meta` error ([Errors](#errors)) instead.

To read console output, install this console capture after each page load. It records later console calls,
uncaught errors and unhandled rejections; read them with `__DC_CONSOLE__.slice(-20)`. For errors from page load,
install it, then repeat the action (`invalidateAll` in [Change state](#change-state), or an in-app navigation).

```js
// run in page — console capture
() => {
  if (globalThis.__DC_CONSOLE__) return { ok: true, count: __DC_CONSOLE__.length };
  const log = (globalThis.__DC_CONSOLE__ = []);
  const text = (a) => {
    if (a instanceof Error) return a.stack ?? String(a);
    if (typeof a === 'string') return a;
    try { return JSON.stringify(a); } catch { return String(a); }
  };
  const push = (level, args) => {
    log.push({ level, time: Date.now(), text: args.map(text).join(' ') });
    if (log.length > 200) log.splice(0, 100);
  };
  for (const level of ['log', 'info', 'warn', 'error', 'debug']) {
    const orig = console[level];
    console[level] = (...args) => { push(level, args); return orig.apply(console, args); };
  }
  addEventListener('error', (e) => push('uncaught', [e.error ?? e.message]));
  addEventListener('unhandledrejection', (e) => push('unhandledrejection', [e.reason]));
  return { ok: true, count: 0 };
}
```

## Where to Start

| Symptom                                        | Check first                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Data never appears or stays loading            | The key's `meta` error ([Errors](#errors)) and its request in the network log                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| A mutation succeeded but the UI shows old data | The mutation key's `meta` error: a response that fails to normalize, such as one wrapped in `{ data }`, is stored there, not on the action. Then `state.entities`: if the entity is unchanged and the response sits only under the mutation's own `endpoints` key, the endpoint has no `schema`. A created item missing from a list: compare the list's key in `state.endpoints` with the args the create or `getList.push` was called with; how a Collection matches lists is in skill "data-client-schema"                                                                               |
| A change shows, then reverts on its own        | The mutation key's `meta` error: an optimistic change reverts when its request fails. If there is none, repeat the change and read the log by time ([Actions](#actions)) for a later `rdc/setresponse` with a newer `meta.fetchedAt` (often a poll) that carried the old value; if it carried an older version of the entity, the fix is in that Entity's definition (skill "data-client-schema"). If the server already returns the new value, something between served the old one: the browser's HTTP cache answers with `transferSize` `0` and a nonzero `bodySize` (requests snippet) |
| Requests fire too often                        | Request times in the network log, then the log ([Actions](#actions)) for that key: whether an `rdc/invalidate` or `rdc/invalidateall` comes before each fetch (something, often a Manager, keeps invalidating it). `pollFrequency` is in milliseconds                                                                                                                                                                                                                                                                                                                                      |
| Requests never fire                            | The key's `meta.expiresAt`: data stays fresh until then. A polled key also needs an `rdc/subscribe` in the log                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |

## The Store

Snippets use the entity `Todo` and paths containing `/todos`: replace them with the app's own.

- **Several stores** (`__DC__.controller` throws `Set __DC__.store to one of: ...`): set `__DC__.store` to the key
  in `__DC__.stores` whose `getState().endpoints` holds the keys you are debugging. A page load clears it.
- **`__DC__` is undefined**: the page is a production build, its managers leave out `DevToolsManager`, or its
  `@data-client` packages are older than 0.19.

## Read State

A key's denormalized response, as components see it. The action log holds each fetched key's endpoint and `args`,
so this works for any key read since the page loaded:

```js
// run in page — response for a key
() => {
  const c = __DC__.controller, state = c.getState();
  // a fetch of the key, not a mutation of an item under it
  const a = __DC__.actions.findLast(
    a => a.args && !a.endpoint?.sideEffect && a.key?.includes('/todos'),
  );
  if (!a) return { error: 'key not in the action log; repeat the fetch' };
  const { data, expiryStatus, expiresAt } = c.getResponse(a.endpoint, ...a.args, state);
  return { key: a.key, data, expiryStatus, expiresAt, error: c.getError(a.endpoint, ...a.args, state)?.message };
}
```

`expiryStatus`: `1` Invalid, `2` InvalidIfStale, `3` Valid. To fetch it again and read the fresh response:

```js
// run in page — refetch a key
async () => {
  const c = __DC__.controller;
  // skip mutations: fetching their endpoint would send the change again
  const a = __DC__.actions.findLast(
    a => a.args && !a.endpoint?.sideEffect && a.key?.includes('/todos'),
  );
  if (!a) return { error: 'key not in the action log' };
  await c.fetch(a.endpoint, ...a.args);
  return c.getResponse(a.endpoint, ...a.args, c.getState()).data;
}
```

Raw normalized state, for keys no longer in the log or to see how entities are stored (shape under
[State Shape](#state-shape-reference)):

```js
// run in page — cache entries by path substring
() => {
  const state = __DC__.controller.getState();
  return Object.keys(state.endpoints).filter(k => k.includes('/todos')).map(key => ({
    key, value: state.endpoints[key], meta: state.meta[key],
  }));
}
```

`getState().entities.Todo` holds every `Todo` by pk.

## Actions

The log keeps the newest 100 actions, and each polling key (endpoint and args) only its last few. Only the newest
20 keep their `response`; to see a payload, repeat the action and read it right away. Entries are dropped from the
front, so select by time (`meta.fetchedAt >= start`), not by index. Actions hold endpoints and promises, so map
them to plain fields:

```js
// run in page — recent actions
() => __DC__.actions.slice(-20).map(a => ({
  type: a.type,
  key: a.key,
  endpoint: a.endpoint?.name,
  args: a.args,
  fetchedAt: a.meta?.fetchedAt,
  error: a.error ? (a.response?.message || true) : undefined,
}))
```

### Errors

Errors thrown while storing a response (for example a missing primary key) land only in `meta`, with no `error`
on the action, so check both:

```js
// run in page — errors
() => ({
  actions: __DC__.actions
    .filter(a => a.error)
    .map(a => ({ type: a.type, key: a.key, error: a.response?.message || true })),
  meta: Object.entries(__DC__.controller.getState().meta)
    .filter(([, m]) => m.error)
    .map(([key, m]) => ({ key, error: m.error.message, status: m.error.status })),
})
```

## Change State

Use Controller methods, never raw dispatches: they log the same actions the app's own calls do.

```js
// run in page — invalidate keys by path substring
() => {
  __DC__.controller.invalidateAll({ testKey: key => key.includes('/todos') });
  return { invalidated: true };
}
```

`expireAll` takes the same `testKey` and marks keys stale without refetching until next use.
`setResponse(a.endpoint, ...a.args, value)` with a logged action injects mock data, and `resetEntireStore()`
clears everything.

## State Shape Reference

```ts
State = {
  entities: { [entityKey: string]: { [pk: string]: EntityInstance } },
  endpoints: { [cacheKey: string]: PK | PK[] | unknown },
  indexes: { [entityKey: string]: { [indexName: string]: { [lookupValue: string]: PK } } },
  meta: {
    [key: string]: {
      date, fetchedAt, expiresAt,
      prevExpiresAt?, error?, invalidated?, errorPolicy?: 'hard' | 'soft'
    }
  },
  entitiesMeta: { [entityKey: string]: { [pk: string]: { date, expiresAt, fetchedAt } } },
  optimistic: (SetResponseAction | OptimisticAction)[],
  lastReset: number,
}
```

## Manual debugging

If the user wants to inspect the store themselves, send them to the Redux DevTools extension steps at
<https://dataclient.io/vue/getting-started/debugging#manual-debugging>.
