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”.
Reading the log
Section titled “Reading the log”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.
What a change was, before and after
Section titled “What a change was, before and after”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 aspostgres://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 }, withchangedtoo 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 saysby: operator.
Walkthrough: review the audit log
Section titled “Walkthrough: review the audit log”Filter by action, open an entry and clear the filters.
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.

Narrow the list with Action, User, From and To, then choose Apply. Here the list is limited to dashboards that were created.

Select a row to open Entry Details for that change: its time, the person and the action.

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

The company audit view
Section titled “The company audit view”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-auditlists the trail, filtered byscope=company(only actions outside any workspace),workspaceId(one of your company’s),userId,action,resourceType,fromandto, paged withlimit(up to 200) andoffset. It reads the live log: the last two years.GET /api/company-audit/exportservesformat=csvwith the same filters andlimitup to 100,000: when more rows match, the export is refused, naming the limit, rather than cut short, so narrow the range.format=ndjsonis the SIEM feed of the company’s trail, resumed withafterSeqfrom theX-Last-Seqheader, up to 10,000 lines a page. A collector polls it with an API key in theX-API-Keyheader: one a company admin made, with theread(oradmin_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.
Tamper-evidence
Section titled “Tamper-evidence”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_hashorrow_hashhas 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.
Verifying it
Section titled “Verifying it”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.
Exporting
Section titled “Exporting”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.
CSV — for a human reviewer
Section titled “CSV — for a human reviewer”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.
NDJSON — for a SIEM
Section titled “NDJSON — for a SIEM”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.
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.
Retention
Section titled “Retention”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.
Query executions
Section titled “Query executions”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.
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.
Related
Section titled “Related”- Log Explore — exploring your own event and log data; a different feature from this platform audit trail.
- Users, roles & invites — where the
admin_auditpermission is granted. - Webhooks — pushing events out as they happen, rather than pulling a record afterwards.
- REST API — the full endpoint reference.