Skip to content
datasquares.aiSign inStart free

Metrics & Governance

Audit log

DataSquares records the actions that matter for governance — who shared what, who changed a data source, who issued an API key, who signed in — into a workspace audit log. It is designed for the question an auditor actually asks, which is not “what happened” but “how do I know this record wasn’t edited afterwards”.

Admin → Audit log lists the workspace’s events, 20 at a time (or 50 or 100), newest first, with Refresh: Timestamp, User (the user’s id), Action, Resource and IP Address. Filter by Action (All Actions, or one of dashboard.created, dashboard.updated, dashboard.deleted, user.login, user.logout, user.register, user.invited, user.created, user.updated, user.deleted and user.role_changed: the list holds every action, but only these can be picked here), User (a user id), From and To; click Apply to run the filter and Reset to clear it. Select a row for its detail panel. Access is gated on the admin_audit permission: administrators have it, members need it granted. Reading the audit log is itself governed — you can’t quietly inspect who has been doing what.

Each table row shows time, user, action, resource, and source IP. Selecting it opens Entry Details: the time, user, action, resource and IP address, then What changed for an entry that records a change (each field with its value before and after, described below), and the entry’s other Details, one per line. The API row also carries the user agent.

When an entry records a change to something that already existed, its details also carry change: before and after, holding only the fields that differ (before is null for something created, after for something deleted). For example, a dashboard renamed from Weekly sales to Weekly sales by region has "change": { "before": { "name": "Weekly sales" }, "after": { "name": "Weekly sales by region" } }. It sits next to the entry’s other details, so it is covered by the hash chain and appears in the CSV export and the NDJSON feed like the rest of the details. In Entry Details it is a table of Field, Before and After: a field inside a group of settings by its dotted name (placement.<menu id>.sort_order), nothing as a dash, a fingerprint as its first characters and its size, and a created item’s values under What it was created with.

  • Secrets are never written down. A password, client secret, token or key reads [redacted] on both sides, so the entry shows that it changed and nothing more; so does the password in an address such as postgres://user:password@host.
  • Large content is fingerprinted. A dashboard’s layout and filters and a pipeline’s steps are recorded as a SHA-256 fingerprint of what was saved (layout_sha256, filter_state_sha256, dag_sha256): enough to tell whether they changed, and which version was in place, without copying them into the log. A theme’s tokens are recorded the same way, with the names of the tokens that changed: "tokens": { "sha256": "…", "bytes": 1840, "changed": ["dataColors"] }. So is any other value longer than 4 KB, such as a long description or a long list of metric filters: { "sha256": "…", "bytes": 160000 }, with changed too when the value is a group of settings.
  • Which entries carry it: changes to data sources, dashboards and their themes, dashboard filters, bookmarks (their saved filters as a fingerprint, never the values), metrics, metric digests, model measures, pipelines and their schedules (including the steps the DataSquares AI assistant adds, as a fingerprint), dataset schedules, asset ownership, glossary terms and their links, a story’s parameter settings, the recovery window, the workspace’s story snapshot setting, an applied license, and the billing settings for overage, plan overage, AI credits auto top-up and AI credits below zero. Other entries keep the details they always had; your billing details (company name, address and tax ID) do not carry a record yet.
  • Company-wide changes are in the company audit view. Changes made for the whole company (people and their roles and attributes, your own profile, the security policy, the SSO connection, the billing country, menu entries, the menu order and a role’s menu labels) are recorded with the same before and after, but they belong to no workspace, so a workspace’s log does not list them. On Enterprise, a company admin sees them in the company audit view.
  • Plans an operator grants or removes are listed. When DataSquares activates or ends a product plan for your workspace by hand, the entry (plan.granted, plan.revoked) names no person and says by: operator.

Filter by action, open an entry and clear the filters.

  1. Open Admin, then Audit log. Every change made in the workspace is listed newest first: when it happened, who did it, what they did and to which item.

    Open Admin, then Audit log. Every change made in the workspace is listed newest first: when it happened, who did it, what they did and to which item.
  2. Narrow the list with Action, User, From and To, then choose Apply. Here the list is limited to dashboards that were created.

    Narrow the list with Action, User, From and To, then choose Apply. Here the list is limited to dashboards that were created.
  3. Select a row to open Entry Details for that change: its time, the person and the action.

    Select a row to open Entry Details for that change: its time, the person and the action.
  4. Choose Reset to clear the filters and see everything again. Export, at the top right, downloads what is listed.

    Choose Reset to clear the filters and see everything again. Export, at the top right, downloads what is listed.

A company admin can switch Admin → Audit Log from This workspace to Whole company: every workspace’s actions and the ones that belong to no workspace (people and their roles and attributes, sign-ins, the company profile, the security policy, single sign-on, menus and billing) in one list, newest first. Where names the workspace, or reads Company for an action outside any workspace. Only actions outside any workspace narrows the list to those; Action (any action name, typed, such as company.sso_config_updated), From and To filter it, with Apply. Export company CSV downloads it.

Below Enterprise the switch carries an Enterprise badge and opens a prompt instead. A member who is not a company admin never sees it, whatever their admin_audit permission: the view spans every workspace and the company’s people.

Export company CSV downloads the trail as the server writes it, the 7-year archive included, with each row’s chain position and whether it is still in the live log. The API:

  • GET /api/company-audit lists the trail, filtered by scope=company (only actions outside any workspace), workspaceId (one of your company’s), userId, action, resourceType, from and to, paged with limit (up to 200) and offset. It reads the live log: the last two years.
  • GET /api/company-audit/export serves format=csv with the same filters and limit up to 100,000: when more rows match, the export is refused, naming the limit, rather than cut short, so narrow the range. format=ndjson is the SIEM feed of the company’s trail, resumed with afterSeq from the X-Last-Seq header, up to 10,000 lines a page. A collector polls it with an API key in the X-API-Key header: one a company admin made, with the read (or admin_audit:view) scope. The key stops reading the company’s trail when its owner is no longer a company admin.

Each export is recorded as audit.exported. Rows written before this view existed are matched by their workspace; a company-level action recorded before it (a sign-in, a change to a person) that names neither a workspace nor the company cannot be attributed and is not listed.

Every row is linked to the one before it by a SHA-256 hash chain: each entry stores a hash of its own contents plus the hash of its predecessor. Changing a past row changes its hash, which breaks every link after it.

Three properties make that more than a convention:

  • The chain is computed by the database, in an insert trigger — so every writer is chained, not just the ones that remembered to be. A caller that supplies its own seq, prev_hash or row_hash has those values discarded.
  • UPDATE is refused outright, and so is TRUNCATE. DELETE is refused too, except from the retention sweep described below.
  • Attribution survives a workspace deletion. The workspace is recorded as an immutable, hashed value at write time rather than only as a foreign key, so deleting a workspace cannot make the database rewrite its own history.

GET /api/audit-logs/verify walks the global chain and reports whether it is intact — and if not, the exact seq where it first stopped being intact. It distinguishes what went wrong: a row whose contents no longer match its stored hash (edited), a link that doesn’t match its predecessor (cut or re-spliced), a gap in the sequence (a row removed with nothing accounting for it), a head that doesn’t continue the last recorded anchor (a truncation), and a row whose timestamp moves backwards far enough to be a backdated insert rather than clock skew.

The verifier recomputes the hashes in the API process rather than asking the database whether its own rows are valid — a checker that asks the audited system to grade its own homework would report a clean bill of health on a subverted one.

Verification defaults to 100,000 rows and accepts fromSeq plus a maximum limit of 500,000. A partial: true response means the cap stopped before the current head. The endpoint returns positions, hashes, anchors, and findings—not audit event content from other workspaces. Verification is itself appended as audit.chain_verified after the report is calculated.

The Export → CSV control on the Audit Log screen exports the rows currently loaded on that page. For a complete filtered export, automation, or a SIEM cursor, use the API endpoints below.

GET /api/audit-logs/export serves two formats. CSV takes the list filters; NDJSON is a separate sequence-cursor feed.

A download with a stable column order (id, created_at, workspace_id, user_id, action, resource_type, resource_id, ip_address, user_agent, details). New columns append, so a saved import mapping keeps working.

Audit rows carry attacker-influenced text — user agents, resource names — and spreadsheets execute any cell starting with =, +, -, @ or a tab as a formula. An audit export is therefore a delivery vehicle straight to the reviewer’s machine, so those cells are prefixed with an apostrophe: inert as data, unchanged as text. Exporting is itself audited.

Add format=ndjson for one JSON object per line, with afterSeq as a resume cursor. NDJSON supports only afterSeq and limit; user, action, resource, and date filters are not applied to this format.

Terminal window
curl -H "Authorization: Bearer $TOKEN" \
"https://your-instance/api/audit-logs/export?format=ndjson&afterSeq=$LAST_SEEN"

seq is global and monotonic. The feed is workspace-scoped, so gaps are normal whenever another workspace wrote the intervening global rows; retention can also remove an old prefix. Use the X-Last-Seq response header (or the last line) as the next afterSeq. The query is strictly seq > afterSeq, so a successful page is not delivered twice.

On Enterprise the feed also carries the workspace’s rows the retention sweep moved to the 7-year archive. They are older than every row still in the log, so one afterSeq cursor runs through both, and each line’s kept says where it is: archive or live.

Every line carries seq, prev_hash, and row_hash, but a workspace feed is not a complete copy of the global chain and therefore cannot independently verify every predecessor link. Preserve the feed off-box as evidence and use the global /verify report and exported hourly head anchors for full-chain verification.

NDJSON defaults to 1,000 rows and is capped at 10,000 per request. CSV defaults to 50,000 and is capped at 100,000. Both export actions are recorded as audit.exported.

Entries are kept for AUDIT_RETENTION_DAYS — 730 days (two years) by default, the usual SOC 2 / ISO answer. An hourly sweep removes older rows in bounded batches (up to 5,000 per batch and 20 batches per pass) so it cannot hold one unbounded delete lock.

Set it to 0 to disable deletion entirely — the escape hatch for a deployment under legal hold.

On Enterprise, 7 years. Before the sweep removes a row of a company on Enterprise, it copies the row to an archive in the same statement, with its seq and hashes, so each archived row still checks against its own hash. The archive keeps rows for AUDIT_ARCHIVE_RETENTION_DAYS, 2,557 days (seven years) by default (0 keeps them). The exports read it (the company CSV and both NDJSON feeds); the in-app lists show the live log, the last two years, on every plan. A self-hosted deployment with an Enterprise licence archives every company’s rows. Rows removed before a company moved to Enterprise were not archived, and a workspace’s own CSV export reads only the live log.

The sweep deletes a prefix of the chain rather than a slice by date, and records an anchor as it goes, so verification of the surviving log still succeeds and a truncation is still detectable. It is the only code permitted to delete from the table.

On the same hourly tick, DataSquares witnesses a changed chain head in the anchor table and structured server log. That off-box log copy or backup is what makes a previously witnessed head useful against a database superuser who can otherwise disable triggers and consistently rewrite the chain.

Every query run in the workspace is recorded — never sampled: chart and dashboard cards, SQL, share links, exports, alerts and the AI. Each record says who ran it, on which source, the SQL, how long it took, how many rows came back, whether it was served from cache, and any error. Query records are kept apart from the chained audit log above, so recording them never slows a query down.

GET /api/audit-logs/queries lists them newest first, filtered by userId, sourceId, origin, errorsOnly, from and to, paged with limit (up to 200) and offset. It needs the same admin_audit view permission.

For a SIEM, GET /api/audit-logs/queries/export returns NDJSON, oldest first. Pass the X-Next-Cursor response header back as after to resume: a successful page is never delivered twice. The feed trails the present by a few seconds, so a query that finishes a moment late is not skipped. limit defaults to 1,000 and is capped at 10,000. Each pull is recorded as audit.exported.

Terminal window
curl -H "Authorization: Bearer $TOKEN" "https://your-instance/api/audit-logs/queries/export?after=$NEXT_CURSOR"

Query records are kept for QUERY_LOG_RETENTION_DAYS — 365 days by default, the year that warehouse query histories keep. The hourly sweep removes older ones in bounded batches; 0 keeps everything.

  • Log Explore — exploring your own event and log data; a different feature from this platform audit trail.
  • Users, roles & invites — where the admin_audit permission is granted.
  • Webhooks — pushing events out as they happen, rather than pulling a record afterwards.
  • REST API — the full endpoint reference.