# Audit Trail History
URL: https://pro.rv-grid.com/guides/data-manage/audit-history/
Source: src/content/docs/guides/data-manage/audit-history.mdx
Description: Track, review, persist, restore, and export accountable RevoGrid data changes.
`AuditHistoryPlugin` records accountable data-change transactions for spreadsheet-like business workflows. Use it when the grid edits important records and your application needs to answer who changed what, when it changed, and how to recover it.

`HistoryPlugin` owns local undo and redo. `AuditHistoryPlugin` owns the reviewable change log, user metadata, persistence hooks, restore actions, and audit panel. Audit History automatically reuses an existing `HistoryPlugin` or installs one when needed; use `grid.history` when you need to configure History behavior.

## What To Try

The demo is wired like a production audit surface: edit cells, select a changed cell, filter the history, export records, and restore previous values from the panel.

    Change a cell and watch the panel add a timestamped record with user and before/after value details.

    Hover a marked cell to see the latest change summary while keeping the full audit trail in the panel.

    Click a cell or row to switch between focused cell history, row history, and full-grid history.

    Use restore actions to roll back a cell, row snapshot, or transaction while recording the restore itself.

## Why Use It

When users edit operational data, the final cell value is not enough. Audit history keeps the context around each change so business users, admins, and support teams can review the full lifecycle of a value.

    Record the current user, timestamp, action type, row identity, changed column, previous value, and new value.

    Group paste and range-edit operations into one transaction instead of treating every changed cell as an unrelated record.

    Restore a single cell, a row snapshot, or a complete transaction after accidental edits.

    Keep records in memory, browser `localStorage`, or a custom backend adapter.

## When To Use It

Audit history is useful when RevoGrid edits real business data, not temporary UI state.

    Track changes to orders, invoices, stock levels, delivery statuses, prices, and approvals.

    Review manual adjustments to budgets, forecasts, risk values, and reporting tables.

    Log edits to permissions, limits, subscriptions, account status, and configuration records.

    Explain why a value changed without guessing from the current dataset.

## Basic Setup

### Cell Change Indicators

The Audit History examples also mark edited cells directly in the grid. When a cell has recorded changes, the cell shows a small upper-right corner marker aligned to the cell edge. Hovering the cell uses `TooltipPlugin` to show the change count, latest before/after value, user, and timestamp.

This is built on existing plugin APIs: resolve the `AuditHistoryPlugin` instance, call `getCellHistory(rowId, column)` from cell rendering hooks, add tooltip attributes through `cellProperties`, and refresh visible rows when audit events fire.

```ts
const records = auditPlugin.getCellHistory(row.id, column.prop);
```

The marker is only a visual affordance. The audit panel remains the complete review and restore surface.

Use `AuditHistoryPlugin` with `EventManagerPlugin`. The event manager normalizes cell edits, range edits, paste operations, undo, redo, and audit restore replays into edit events the audit plugin and other edit integrations can observe.

Audit History delegates range-shaped restore updates through the existing History replay path. That means cell restore, row replacement restore, and transaction restore emit `beforehistedit`; `EventManagerPlugin` turns that into the normal `gridedit` flow; and `CellFlashPlugin` can flash restored cells from the same edit pipeline. Row insertion and snapshot restore still use direct source/data updates because they are not pure range patches.

1. Import the plugins, panel helper, and config type.

   ```ts
   import {
     AuditHistoryPlugin,
     CellFlashPlugin,
     EventManagerPlugin,
     createLocalStorageAuditHistoryAdapter,
     defineAuditHistoryPanel,
     type AuditHistoryConfig,
   } from '@revolist/revogrid-pro';
   ```

2. Provide the audit config.

   ```ts
   const auditHistory: AuditHistoryConfig = {
     getCurrentUser: () => ({
       id: 'avery-stone',
       name: 'Avery Stone',
       email: 'avery.stone@company.com',
     }),
     rowIdProp: 'id',
     storage: 'memory',
     onAuditRecord(record) {
       // Persist to your API, Supabase, Firebase, PostgreSQL, etc.
       console.log(record);
     },
   };
   ```

3. Register plugins and bind the config directly on the grid.

   ```ts
   grid.plugins = [EventManagerPlugin, AuditHistoryPlugin, CellFlashPlugin];
   grid.auditHistory = auditHistory;
   ```

   `HistoryPlugin` does not need to be listed for normal Audit History restore behavior. Add it explicitly only when the page also has direct History UI controls or you want to control plugin order yourself.

4. Mount the optional review panel.

   ```ts
   const auditPanel = defineAuditHistoryPanel(panelElement, grid, {
     pageSize: 25,
     allowExport: true,
     placement: 'right',
     restoreActions: ['cell', 'row'],
     formatDate: (value, { mode }) =>
       new Intl.DateTimeFormat(undefined, mode === 'day'
         ? { weekday: 'short', month: 'short', day: 'numeric' }
         : { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }).format(new Date(value)),
     events: {
       beforeExport: ({ format }) => format !== 'json',
       beforeRestore: ({ type }) => type !== 'transaction',
     },
   });

   // Later:
   auditPanel.setPlacement('bottom');
   auditPanel.refresh();
   ```

The plugin still observes `additionalData.auditHistory` for older integrations, but new code should use the direct `grid.auditHistory` property.

## Stable Row Identity

Audit records should point to business records, not visual row positions. Row indexes can change after sorting, filtering, grouping, or lazy loading, so every row should expose a stable identity.

```ts
const rows = [
  { id: 'invoice-123', status: 'Draft', amount: 1200 },
  { id: 'invoice-124', status: 'Paid', amount: 800 },
];

const auditHistory: AuditHistoryConfig = {
  rowIdProp: 'id',
};
```

If your row identity is derived, use `getRowId` instead:

```ts
const auditHistory: AuditHistoryConfig = {
  getRowId: (row) => `${row.tenantId}:${row.invoiceNumber}`,
};
```

## What Gets Recorded

Each top-level audit item is an `AuditRecord`. A record contains one transaction ID and one or more `AuditChange` entries.

```ts
{
  id: 'audit-001',
  transactionId: 'tx-001',
  type: 'cell-change',
  changedAt: '2026-05-08T12:00:00Z',
  changedBy: {
    id: 'avery-stone',
    email: 'avery.stone@company.com',
  },
  changes: [
    {
      id: 'change-001',
      rowId: 'invoice-123',
      rowType: 'rgRow',
      column: 'status',
      oldValue: 'Draft',
      newValue: 'Approved',
    },
  ],
}
```

For a paste operation, the same record can contain many changes:

```ts
{
  transactionId: 'tx-002',
  type: 'bulk-paste',
  changedAt: '2026-05-08T12:05:00Z',
  changedBy: { id: 'avery-stone' },
  changes: [
    {
      id: 'change-002',
      rowId: 'invoice-123',
      rowType: 'rgRow',
      column: 'status',
      oldValue: 'Draft',
      newValue: 'Approved',
    },
    {
      id: 'change-003',
      rowId: 'invoice-124',
      rowType: 'rgRow',
      column: 'status',
      oldValue: 'Draft',
      newValue: 'Rejected',
    },
  ],
}
```

## Storage Options

    Use `storage: 'memory'` for demos, temporary sessions, and test fixtures.

    Use `storage: 'localStorage'` when browser persistence is enough for the workflow.

    Use `storage: 'custom'` with `storageAdapter` when your backend should load, append, save, or clear records.

    Use `storageProviders` to list cache or server providers. Provider loads refresh the panel automatically.

    Use `maxRecords` to bound retained records. The implementation defaults to `1000`.

For production systems, prefer a trusted server provider. If you include several providers, the default is intentionally isolated: the first provider that returns records owns the loaded audit trail, and later providers with records are ignored with a console warning. Set `mergeStorageProviders: true` only when your app deliberately wants to merge records by id across providers.

```ts
const auditHistory: AuditHistoryConfig = {
  getCurrentUser: () => currentUser,
  rowIdProp: 'id',
  storageProviders: [
    {
      async load() {
        const response = await fetch('/api/audit-history');
        return response.json();
      },
      async append(record) {
        await fetch('/api/audit-history', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(record),
        });
      },
      async save(records) {
        await fetch('/api/audit-history', {
          method: 'PUT',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(records),
        });
      },
      async clear() {
        await fetch('/api/audit-history', { method: 'DELETE' });
      },
    },
  ],
};
```

Provider write failures emit `auditerror` but do not block editing. Configuring `storageProviders` together with `storage` or `storageAdapter` shows a console warning because `storageProviders` takes precedence.

When you need a deliberate cache-plus-server merge, opt in explicitly:

```ts
const auditHistory: AuditHistoryConfig = {
  rowIdProp: 'id',
  mergeStorageProviders: true,
  storageProviders: [
    createLocalStorageAuditHistoryAdapter('invoices:audit-history'),
    serverAuditHistoryAdapter,
  ],
};
```

### Provider Load Flow

Provider chains are useful when the user should see cached history quickly while a server-backed provider loads the durable records.

1. The panel mounts and reads the current in-memory records.

2. Storage providers load in order. By default, the first provider that returns records owns the loaded audit trail.

3. The plugin emits `auditrecordsloaded` whenever loaded records are applied.

4. The built-in panel refreshes on `auditrecordsloaded`, `auditrecord`, and `auditrestore`, so async loads and external restore controls stay visible without polling.

Client-side records are excellent for review panels and recovery UX. Compliance-grade audit trails should be written to a trusted backend.

## Privacy And Policy Controls

Production audit logs often need more than raw before/after values. Use `ignoredColumns`, `sanitizeValue`, `getMetadata`, `canRestore`, and `immutable` to align the client behavior with your business rules.

```ts
const auditHistory: AuditHistoryConfig = {
  getCurrentUser: () => currentUser,
  rowIdProp: 'id',
  ignoredColumns: ['internalNotes'],
  sanitizeValue(value, context) {
    if (context.column === 'amount') {
      return '[redacted]';
    }
    return value;
  },
  getMetadata() {
    return {
      workspaceId: currentWorkspace.id,
      requestId: crypto.randomUUID(),
    };
  },
  canRestore({ type }) {
    return currentUser.role === 'admin' && type !== 'transaction';
  },
};
```

Use action-type policy when entire classes of audit records should be included or ignored:

```ts
const auditHistory: AuditHistoryConfig = {
  ignoredActionTypes: ['undo', 'redo'],
  trackedActionTypes: ['cell-change', 'bulk-paste', 'approval-submitted'],
  shouldTrackRecord({ type, changes, metadata }) {
    if (type === 'server-validation' && metadata?.severity === 'debug') {
      return false;
    }
    return changes.length > 0 || type === 'approval-submitted';
  },
};
```

For compliance-style screens where the client must not mutate local audit records, set `immutable: true`. This disables `clear`, `replaceRecords`, and restore actions in the plugin. Server-side immutability should still be enforced by your backend.

```ts
const auditHistory: AuditHistoryConfig = {
  storage: 'custom',
  storageAdapter,
  immutable: true,
};
```

## Review And Restore

The audit panel lets users inspect grid changes without leaving the page. Common review flows include full-grid history, selected-row history, selected-cell history, user filtering, date filtering, action-type filtering, and old/new value comparison.

    Switch between full-grid, selected-row, and selected-cell history without changing the underlying audit records.

    The panel refreshes after new audit records, provider-loaded batches, and restore events from other UI controls.

    `canRestore`, `beforeauditrestore`, and `immutable` let the app decide which restore actions users can run.

    Generate filtered CSV or JSON exports for reporting, support workflows, or backend handoff.

Use the plugin instance when you need audit data in custom UI:

```ts
const plugins = await grid.getPlugins();
const auditHistoryPlugin = plugins.find(
  (plugin) => plugin instanceof AuditHistoryPlugin,
);

const records = auditHistoryPlugin?.getRecords({ userId: 'avery-stone' });
const cellHistory = auditHistoryPlugin?.getCellHistory('invoice-123', 'status');
const rowHistory = auditHistoryPlugin?.getRowHistory('invoice-123');
const lastStatusChange = auditHistoryPlugin?.getLastChange('invoice-123', 'status');
const stats = auditHistoryPlugin?.getStats();
```

Restore helpers return `true` when a restore was applied:

```ts
auditHistoryPlugin?.restoreCell(change);
auditHistoryPlugin?.restoreRow('invoice-123');
auditHistoryPlugin?.restoreTransaction(transactionId);
```

Cell, row replacement, and transaction restores are replayed through `HistoryPlugin.replayChange()`. The restore itself is still recorded as `restore-cell`, `restore-row`, or `restore-transaction`, but the replay is guarded so it does not create a second normal edit record.

Use `recordEvent` for application-driven records that do not come from direct grid editing:

```ts
auditHistoryPlugin?.recordEvent({
  type: 'api-sync',
  changedBy: { id: 'webhook', name: 'Webhook' },
  presentation: {
    source: 'api',
    targetLabel: 'F1:F8',
    detailLabel: '8 cells recalculated',
  },
});
```

Custom action types are plain strings. When the record carries normal cell or row changes, existing restore helpers can use it for revert workflows:

```ts
auditHistoryPlugin?.recordEvent({
  type: 'approval-submitted',
  transactionId: 'approval-42',
  changedBy: currentUser,
  changes: [{
    rowId: 'invoice-123',
    rowIndex: 0,
    rowType: 'rgRow',
    column: 'status',
    oldValue: 'Draft',
    newValue: 'Approved',
  }],
  presentation: {
    source: 'api',
    verb: 'submitted approval',
    targetLabel: 'Invoice 123',
  },
});

auditHistoryPlugin?.restoreTransaction('approval-42');
```

Summary-only custom records are ignored unless you opt in with `allowEmpty: true`.

Use snapshots when users need a named restore point:

```ts
const snapshot = auditHistoryPlugin?.createSnapshot('Before Q4 close');
if (snapshot) {
  auditHistoryPlugin?.restoreSnapshot(snapshot);
}
```

Query helpers return defensive copies, so application code cannot accidentally mutate the plugin's internal audit log.

## Loading And Export

Use `refreshRecords` when your custom storage adapter can load existing records from a backend. A single `storageAdapter` still supports merge-by-id refreshes by default; pass `{ merge: false }` to replace the local set. Provider chains follow the configured provider policy: isolated by default, merged only with `mergeStorageProviders: true`.

```ts
await auditHistoryPlugin?.refreshRecords();
await auditHistoryPlugin?.replaceRecords(serverRecords, { merge: false });
```

Use `exportRecords` for custom export buttons or reporting workflows:

```ts
const csv = auditHistoryPlugin?.exportRecords({
  format: 'csv',
  filter: { actionType: 'bulk-paste' },
  includeMetadata: true,
});

const json = auditHistoryPlugin?.exportRecords({ format: 'json' });
```

Export failures are reported through `auditerror` with phase `export` and return an empty string. That keeps export buttons from crashing the page while still giving your app a single error channel for observability.

The built-in panel docks to the right of the grid by default and can be pinned with `placement: 'left' | 'right' | 'top' | 'bottom' | 'none'`. It includes timeline day grouping, search, user/column/action/date filter chips, scoped cell and row views, pagination, CSV/JSON export controls, hover restore/jump actions, row-click compare mode, optional mini close state, and i18n-friendly labels. Use `restoreActions`, callback-capable `allowCompare`, `formatDate`, and `events` to control panel actions. Pass `tooltips: false` to hide all help icons, or override/hide individual entries with `tooltips: { searchFilter: 'Find a record', clearFilters: false }`. Pass `labels` to rename visible text, placeholders, aria labels, action names, source labels, and dynamic strings such as pagination or selected row/cell summaries.

Listen for `beforeauditrestore` to validate or block a restore, and `auditrestore` after a restore succeeds. New audit records are emitted through `auditrecord`. Storage and hook failures are emitted through `auditerror`; loaded record batches are emitted through `auditrecordsloaded`.

## Production Flow

```txt
User edits grid
  -> EventManagerPlugin normalizes the edit
  -> AuditHistoryPlugin creates an audit transaction
  -> the record is stored locally or sent to your backend
  -> users review changes in the audit panel
  -> admins restore cells, rows, or transactions when needed
  -> range-shaped restores replay through HistoryPlugin and EventManagerPlugin
```

## Summary

`AuditHistoryPlugin` adds accountability to editable RevoGrid applications. Pair it with stable row IDs, a current-user provider, and a trusted persistence path when users edit important operational data.
