# Advisor Inspector

The Advisor Inspector tool allows your app to contribute a check to the Crowdin Advisor and report insights about translation quality, glossary consistency, brand tone, or any other custom signal.

## Sample

```js ins="advisorInspector" title="index.js"
    import crowdinModule, { reportAdvisorInsight } from '@crowdin/app-project-module';

    // In-memory dedup: BE may fire multiple nudges within a short window
    // (install + project-create + user-recheck). Keep only one in flight.
    const inFlight = new Set();

    const configuration = {
      baseUrl: 'https://123.ngrok.io',
      clientId: 'clientId',
      clientSecret: 'clientSecret',
      name: 'Sample App',
      identifier: 'sample-app',
      description: 'Sample App description',
      port: 8080,
      dbFolder: import.meta.dirname,
      imagePath: import.meta.dirname + '/logo.png',
      advisorInspector: [
        {
          key: 'daily-llm-check',
          title: 'Daily LLM Check',
          summary: 'Looks at recent translations and flags anything suspicious.',
          severity: 'medium',
          category: 'context',
          refreshPolicy: 'daily',
          uiPath: import.meta.dirname + '/public',
          onRecheck: async ({ projectId, moduleKey, inspectorId, crowdinId }) => {
            const dedupKey = `${projectId}:${moduleKey}`;
            if (inFlight.has(dedupKey)) return;
            inFlight.add(dedupKey);

            try {
              // ...simulate your async/queued check (could take minutes or hours)...
              await new Promise((resolve) => setTimeout(resolve, 30_000));

              await reportAdvisorInsight(configuration, {
                crowdinId,
                projectId,
                moduleKey,
                outcome: 'flagged',
                metrics: [
                  { key: 'suspicious-strings', value: 7, unit: 'count', tone: 'danger' }
                ],
                recommendations: [
                  { id: 'review-recent-translations', primary: true }
                ],
                payload: { reviewedAt: new Date().toISOString() },
              });
            } finally {
              inFlight.delete(dedupKey);
            }
          }
        }
      ]
    };

    crowdinModule.createApp(configuration);
    ```
  ```ts ins="advisorInspector" title="index.ts"
    import crowdinModule, { reportAdvisorInsight } from '@crowdin/app-project-module';
    import type { Config } from '@crowdin/app-project-module';

    // In-memory dedup: BE may fire multiple nudges within a short window
    // (install + project-create + user-recheck). Keep only one in flight.
    const inFlight = new Set();

    // Config (not ClientConfig): reportAdvisorInsight requires the strict
    // variant with credentials, port, dbFolder and imagePath present.
    const configuration: Config = {
      baseUrl: 'https://123.ngrok.io',
      clientId: 'clientId',
      clientSecret: 'clientSecret',
      name: 'Sample App',
      identifier: 'sample-app',
      description: 'Sample App description',
      port: 8080,
      dbFolder: import.meta.dirname,
      imagePath: import.meta.dirname + '/logo.png',
      advisorInspector: [
        {
          key: 'daily-llm-check',
          title: 'Daily LLM Check',
          summary: 'Looks at recent translations and flags anything suspicious.',
          severity: 'medium',
          category: 'context',
          refreshPolicy: 'daily',
          uiPath: import.meta.dirname + '/public',
          onRecheck: async ({ projectId, moduleKey, inspectorId, crowdinId }) => {
            const dedupKey = `${projectId}:${moduleKey}`;
            if (inFlight.has(dedupKey)) return;
            inFlight.add(dedupKey);

            try {
              // ...simulate your async/queued check (could take minutes or hours)...
              await new Promise((resolve) => setTimeout(resolve, 30_000));

              await reportAdvisorInsight(configuration, {
                crowdinId,
                projectId,
                moduleKey,
                outcome: 'flagged',
                metrics: [
                  { key: 'suspicious-strings', value: 7, unit: 'count', tone: 'danger' }
                ],
                recommendations: [
                  { id: 'review-recent-translations', primary: true }
                ],
                payload: { reviewedAt: new Date().toISOString() },
              });
            } finally {
              inFlight.delete(dedupKey);
            }
          }
        }
      ]
    };

    crowdinModule.createApp(configuration);
    ```
  ## Configuration

| Parameter | Description | Allowed values | Default value |
|---|---|---|---|
| `key` | Raw module key. Becomes the URL segment for the recheck/insight endpoints and the value of `jwt.module`. | String | derived from `identifier` |
| `title` | Header shown in the Advisor card. | String | – |
| `summary` | Optional summary line under the title. ≤70 chars. | String | – |
| `severity` | Visual weight of the insight. | `'high' \| 'medium' \| 'low'` | – |
| `category` | Inspector category. | `'context'` | – |
| `refreshPolicy` | How often the BE may send recheck nudges. | `'hourly' \| 'daily'` | – |
| `onRecheck` | Async callback invoked when Crowdin pushes a recheck nudge. The SDK answers `200` to Crowdin immediately; your callback runs in the background. | Function | – |
| `uiPath` | Path to the iframe content folder (e.g. `import.meta.dirname + '/public'`). | String | – |
| `fileName` | Entry filename inside `uiPath`. | String | `'index.html'` |
| `formSchema` | JSON Schema for an auto-generated form (alternative to `uiPath`). | Object | – |
| `environments` | Restrict the module to `crowdin` and/or `crowdin-enterprise`. | `'crowdin' \| 'crowdin-enterprise'` or array | both |

### `onRecheck` Function

Receives a single object with these properties:

- `projectId` - Numeric Crowdin project ID.
- `moduleKey` - Raw module key (e.g. `daily-llm-check`). Pass this to `reportAdvisorInsight`.
- `inspectorId` - Fully-prefixed `app:<identifier>:<moduleKey>`. Only useful if you need to correlate with Crowdin-side logs.
- `crowdinId` - Tenant identifier. Persist this if you'll call `reportAdvisorInsight` later — the value is the only piece you need to bootstrap a token outside of the callback.
- `client` - Crowdin API client. May go stale after ~1h; for short-lived work only.
- `context` - [Context object](/app-project-module/reference/context/).

Crowdin may fire multiple nudges in a short window (install + project-create + user-triggered recheck). The SDK does **not** dedup for you — key your own work on `projectId + moduleKey`.

## `reportAdvisorInsight` Helper

Reports the result of an inspector run back to Crowdin. Importable from `@crowdin/app-project-module`:

```js
import { reportAdvisorInsight } from '@crowdin/app-project-module';

await reportAdvisorInsight(config, {
  crowdinId,            // from onRecheck args
  projectId,            // from onRecheck args
  moduleKey,            // from onRecheck args (raw key, NOT inspectorId)
  outcome: 'flagged',   // 'flagged' | 'clear' | 'not_applicable'
  metrics: [
    { key: 'demo', value: 1, unit: 'count' }
  ],
  recommendations: [
    { id: 'demo-rec', primary: true, params: { foo: 'bar' } }
  ],
  payload: { foo: 'bar' },
  // timeoutMs: 60_000,  // optional; default 5 minutes
});
```

### Args

- `outcome` — `'flagged' | 'clear' | 'not_applicable'`. Required.
- `metrics` — optional, ≤100, unique by `key`. Each metric:
  - `key` (string, required) — stable identifier within the metrics array.
  - `value` (number, required).
  - `unit` — `'percent' | 'count'`.
  - `threshold` (number) — comparison baseline shown in the UI.
  - `tone` — `'default' | 'success' | 'danger'`.
  - `checkedAt` (RFC3339 string) — per-metric timestamp.
- `recommendations` — optional, ≤20, unique by `id`. Each:
  - `id` (string, required) — opaque to Crowdin; resolved by your iframe.
  - `primary` (boolean).
  - `params` (object) — string-keyed.
- `payload` — optional. Free-form object or array (PHP-array-shaped — indexed or associative). Crowdin rejects raw scalars.
- `checkedAt` (RFC3339 string) — optional top-level timestamp. When omitted, Crowdin sets `checked_at` to now at upsert time. Supplied values are clamped if more than 1 hour in the future or 7 days in the past.
- `timeoutMs` (number) — optional request timeout for the insight PUT. Default `300000` (5 minutes).

The upsert is **full-replace**: omitting `metrics` or `recommendations` clears those fields on the BE side. The helper sends `[]` when you omit them, so passing nothing intentionally clears. If you want to preserve previous values, re-send them.