# Analytics Taxonomy Drift (`immemorial_sarinda/analytics-taxonomy-drift`) Actor

Compare supplied analytics event catalogs for added or removed events, property changes and taxonomy drift. Get one complete deterministic report from a bounded offline comparison. No live analytics access or website fetching.

- **URL**: https://apify.com/immemorial_sarinda/analytics-taxonomy-drift.md
- **Developed by:** [Picknotes](https://apify.com/immemorial_sarinda) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$100.00 / 1,000 completed reports

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Analytics Taxonomy Drift

Compare one versioned tracking plan with one caller-aggregated observation window. Get a complete JSON report of unexpected events and properties, exact type-label mismatches, observed plan versions, and planned events unobserved in the window.

### Scope and privacy

This is an aggregate-only comparison. Supply only authorized, nonpersonal, nonconfidential taxonomy labels and counts. Do not submit raw events, property values, user/device identifier values, personal or sensitive data, secrets, or confidential tracking plans. User-level joins, profiling, remote plan fetching, cross-window trends, and anomaly detection are unsupported. A name such as `userId` is allowed as a taxonomy label; actual identifier values are not.

The platform stores submitted input before validation. Rejection is not a privacy safeguard. Review your platform storage, retention, and access settings before submitting anything. Names are echoed into reports; the tool neither detects all personal data hidden in valid labels nor anonymizes it. Reports also contain counts, window timestamps, versions, source byte lengths, and source hashes. Keep input and output access appropriately restricted.

### Price

USD 0.10 per completed report, including platform usage. One completed report compares one plan with one window; reports with drift findings cost the same. The synthetic demo follows the same paid, complete-report path and costs USD 0.10. It is not a free preview.

A receipt describes a completion-event intent; it is not evidence of payment, settlement, or an independently verified invoice. Its `settlementStatus` is `not_verified`. Check the platform's actual billing records for charges.

### Input

Use JSON input with one of the following shapes. Unknown fields are rejected.

#### Synthetic demo

```json
{"useDemo": true}
```

Do not include `planJson` or `observationsJson` in demo mode. If you include `authorizedNonpersonalNonconfidentialData`, it must be `false` in demo mode. The fixed synthetic example includes type drift, an unexpected property and event, older and unknown versions, and an unobserved planned event.

#### Your own aggregates

Set `useDemo` to `false` and `authorizedNonpersonalNonconfidentialData` to `true`. `planJson` and `observationsJson` must be JSON-encoded strings, not nested objects or URLs. Consent asserts that you are authorized to submit the data and that it is nonpersonal and nonconfidential.

Save this as `plan.json`:

```json
{"schemaVersion":"analytics-taxonomy-plan/1","planVersion":"v1","events":[{"name":"checkout","properties":[{"name":"amount","type":"number"}]}]}
```

Save this as `observations.json`:

```json
{"schemaVersion":"analytics-taxonomy-observations/1","window":{"start":"2026-10-01T00:00:00Z","end":"2026-10-02T00:00:00Z"},"coverage":"partial","totalEventCount":3,"groups":[{"event":"checkout","planVersion":"v1","eventCount":3,"propertyCoverage":"partial","properties":[{"name":"amount","typeCounts":[{"type":"integer","count":2},{"type":"number","count":1}]}]}]}
```

Create `INPUT.json` locally with Python 3, preserving the exact UTF-8 source strings without newline conversion:

```sh
python3 - <<'PY'
import json
from pathlib import Path
value = {
    "useDemo": False,
    "authorizedNonpersonalNonconfidentialData": True,
    "planJson": Path("plan.json").read_bytes().decode("utf-8"),
    "observationsJson": Path("observations.json").read_bytes().decode("utf-8"),
}
raw = json.dumps(value, ensure_ascii=True, separators=(",", ":")).encode("ascii")
if len(raw) > 4_500_000:
    raise SystemExit("Outer input exceeds 4,500,000 bytes")
with open("INPUT.json", "xb") as output:
    output.write(raw)
PY
```

Submit the contents of `INPUT.json` as the Actor input. The example reports two `integer` observations as a mismatch against declared `number`; these labels are intentionally distinct.

### Exact input contract

All fields shown in each plan, observation, group, property, and type-count object are required. Additional fields are rejected. Arrays may be empty.

- Property types are exactly `array`, `boolean`, `integer`, `null`, `number`, `object`, or `string`. These are labels, not nested values; there is no subtype, nullability, or required-property inference.
- Event/property names match ASCII `[A-Za-z_][A-Za-z0-9_.:-]{0,95}`. Versions match ASCII `[A-Za-z0-9][A-Za-z0-9_.-]{0,63}`. Matching is exact and case-sensitive; strings are not trimmed or normalized.
- Group `planVersion` is a version string or `null`. The plan's `planVersion` is a string.
- `coverage` and `propertyCoverage` are exactly `complete`, `partial`, or `unknown`. These are caller assertions, not verified coverage.
- Timestamps are valid Gregorian UTC seconds, exactly `YYYY-MM-DDTHH:MM:SSZ`, years 0001–9999. Start must precede end. The declared window is half-open `[start,end)`. Offsets, fractions, leap seconds, and lowercase timestamp syntax are rejected.
- Use UTF-8 JSON without a BOM. Duplicate object keys, missing/unknown fields, floating-point or exponent numeric tokens, NaN/Infinity, negative integers, and `-0` are rejected. Booleans are not counts. Whitespace counts toward byte limits and changes exact source hashes.
- Plan event names are unique; property names are unique within each event/group; type labels are unique within each property. Observation `(event, planVersion)` pairs are unique, including zero-count groups. Duplicates are rejected, never merged.
- `totalEventCount` equals the sum of every group's `eventCount`, including other and unknown versions. Each property's type counts sum to at most its group's count. You assert at most one type per event for that property. Different properties may overlap; their counts are not summed together against the group denominator.

### How to interpret the report

Version is checked first. Null versions become `unknown-version`; different versions become `version-mismatch`, even at zero count. These groups have no property findings. Only exact matching-version groups are compared.

A zero-count matching-version group is `unobserved-group`, regardless of whether its name is planned. Positive matching groups are `matched-event` or `unexpected-event`. Unexpected events have no property-level findings because there is no corresponding declaration to compare.

Within a positive matched event, a positive-count undeclared property appears in `unexpectedProperties`. A declared property with positive counts of different type labels appears in `typeMismatches`, with its `expectedType` and only the nonmatching positive counts. Zero-only and omitted properties produce no findings. Declaring a property does not make it required.

`observedVersions` aggregates positive-count groups only. These are caller-supplied tracking-plan versions, not proof of deployed SDK/runtime versions. `plannedUnobserved` lists every plan event without a positive matching-version group. Observations under another or unknown version do not remove an event from this list. Unobserved-in-window does not prove missing instrumentation. No event-renaming inference occurs.

Complete property coverage lets you interpret omitted properties or the difference between an event count and a property's counts as absence only under your own completeness assertion. With partial/unknown coverage, absence is unknown. The report never emits missing-property violations under any coverage. Counts describe supplied aggregates, not population rates or extrapolations. Aggregates cannot prove observation timestamps, freshness, completeness, or omitted activity. No CloudEvents or JSON Schema compliance certification is provided.

### Bounds

All bounds must hold together; reaching one maximum does not guarantee the other limits fit.

- Combined exact UTF-8 bytes of `planJson` and `observationsJson`: 2,097,152. There is no separate 1 MiB limit per source.
- Raw outer Actor input: 4,500,000 bytes, including JSON string escaping.
- Plan events: 1,000. Observation groups: 1,000.
- Properties per event or group: 100. Combined property rows across plan and observations: 10,000.
- Type-count rows per observed property: 7.
- JSON container depth per source: 8, root counted as depth 1.
- JSON nodes per source: 150,000; containers and scalar values count, object keys do not.
- Names: 96 ASCII characters. Versions: 64 ASCII characters.
- Each count and `totalEventCount`: 0 through 1,000,000,000; numeric token length at most 10.
- Complete native report: 2,097,152 bytes.
- Actual escaped singleton delivery POST: 4,500,000 bytes. Receipt: 2,048 bytes.

Output must fit in full: no truncation or partial-success report. Bounds failures occur before a completion-event attempt. A failed run is not evidence that no platform usage occurred. Processing is resource-limited: 448 MiB per-process address space, 30/31-second soft/hard CPU limits, a 50-second outer wall limit, and 10-second worker budgets. These are not combined-container resource or guaranteed end-to-end latency promises; OS process creation, termination, and reaping can delay return. Resource failures do not produce a usable partial report.

### Output and extraction

The delivery is one raw dataset item containing `reportJson`, `reportBytes`, `reportSha256`, source metadata, a `deliveryKey`, and a receipt. Export the dataset as raw JSON, preserving all fields and the complete `reportJson` string. Do not use CSV, flattened fields, projections, or a shortened preview. The raw export must contain exactly one item, either as a singleton JSON array or a single JSON object.

The following standalone Python 3 script needs only the standard library and no downloaded helper. Save it as `extract_report.py`. It checks the envelope identifier, report size, exact UTF-8 byte count and SHA-256, and basic report identity before writing a new private file. It does not modify or reserialize report bytes.

```python
import hashlib
import json
import os
import re
import stat
import sys

MAX_EXPORT = 4_500_000
MAX_REPORT = 2_097_152


def unique(pairs):
    result = {}
    for key, value in pairs:
        if key in result:
            raise ValueError("duplicate JSON key")
        result[key] = value
    return result


def reject_constant(value):
    raise ValueError("invalid JSON constant")


def decode(raw):
    text = raw.decode("utf-8")
    if text.startswith("\ufeff"):
        raise ValueError("BOM is not supported")
    return json.loads(text, object_pairs_hook=unique,
                      parse_constant=reject_constant)


def main():
    if len(sys.argv) != 3:
        raise ValueError("usage: extract_report.py raw-dataset.json NEW-report.json")
    # Linux/POSIX: reject final-component symlinks and nonregular input.
    # Use trusted, private parent directories; this is not a filesystem sandbox.
    fd = os.open(sys.argv[1], os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
    with os.fdopen(fd, "rb") as source:
        info = os.fstat(source.fileno())
        if not stat.S_ISREG(info.st_mode) or info.st_size > MAX_EXPORT:
            raise ValueError("invalid or oversized export")
        raw = source.read(MAX_EXPORT + 1)
    if len(raw) > MAX_EXPORT:
        raise ValueError("oversized export")
    item = decode(raw)
    if type(item) is list:
        if len(item) != 1:
            raise ValueError("expected exactly one dataset item")
        item = item[0]
    if type(item) is not dict:
        raise ValueError("expected an object")
    if (item.get("schemaVersion") != "bounded-report-delivery/1"
            or item.get("contractId") != "analytics-taxonomy-drift"
            or item.get("contractVersion") != "0.1"):
        raise ValueError("unexpected delivery identity")
    if type(item.get("reportJson")) is not str:
        raise ValueError("reportJson must be a string")
    report = item["reportJson"].encode("utf-8")
    size = item.get("reportBytes")
    digest = item.get("reportSha256")
    if (type(size) is not int or not 1 <= size <= MAX_REPORT
            or len(report) != size):
        raise ValueError("report byte count mismatch")
    if (type(digest) is not str
            or re.fullmatch(r"[0-9a-f]{64}", digest) is None
            or hashlib.sha256(report).hexdigest() != digest):
        raise ValueError("report SHA-256 mismatch")
    parsed = decode(report)
    if (type(parsed) is not dict
            or parsed.get("schemaVersion") != "analytics-taxonomy-drift-report/1"
            or parsed.get("profile") != "owned-aggregate-taxonomy/1"
            or parsed.get("complete") is not True):
        raise ValueError("unexpected report identity")
    # Exclusive creation: never overwrite an existing path.
    fd = os.open(sys.argv[2], os.O_WRONLY | os.O_CREAT | os.O_EXCL
                 | os.O_NOFOLLOW, 0o600)
    with os.fdopen(fd, "wb") as output:
        if output.write(report) != len(report):
            raise OSError("short write")
        output.flush()
        os.fsync(output.fileno())


if __name__ == "__main__":
    try:
        main()
    except Exception:
        print("Extraction refused or incomplete. No existing file was overwritten. "
              "A newly created file may be incomplete; do not use it.", file=sys.stderr)
        sys.exit(2)
```

Run it on Linux/POSIX from a trusted private directory:

```sh
python3 -I extract_report.py raw-dataset.json NEW-report.json
```

Exit 0 means extraction succeeded; exit 2 means refusal or incomplete writing. The destination must not exist. A write failure can leave a newly created incomplete file, which must not be used. Pretty-printed exports count against the export byte limit; use a compact raw JSON export if necessary, without changing the `reportJson` string.

#### What these checks do and do not prove

This recipe detects a mismatch between the exported report bytes and the envelope's length/hash. It is not the product's full report-only consistency validator and does not reconstruct original-source eligibility. A malicious party can replace both a report and its declared hash. Hashes, receipt fields, and a delivery key are not signatures, sender authentication, or settlement evidence. Obtain the export through your trusted platform account and verify the intended run separately.

Full original-source eligibility requires the exact original plan and observation bytes and the source-based validator, which independently reconstructs the complete expected report. Source hashes alone do not supply those originals. Even a full report-only validator cannot recover omitted plan/property declarations to prove every original comparison. Keep your source files if you need that verification. No extraction check establishes the truth of caller aggregates or coverage assertions.

### Delivery uncertainty

A completion append is attempted at most once per invocation. Delivery is confirmed only by exact readback of the complete singleton item, including after an ambiguous acknowledgement. If the append may have happened but readback cannot establish it, the result remains uncertain and may have been billed. Do not automatically repost or rerun an uncertain delivery: check the existing run's dataset and billing records first to avoid a duplicate report or charge. A successful receipt/readback still does not prove settlement.

# Actor input Schema

## `authorizedNonpersonalNonconfidentialData` (type: `boolean`):

Required as true for own-input mode. Confirm permission to submit both source strings and that neither contains personal, confidential or secret information. In demo mode omit this or set false.

## `observationsJson` (type: `string`):

Own-input mode only: exact UTF-8 JSON string with schemaVersion analytics-taxonomy-observations/1, window, coverage, totalEventCount and groups. Aggregated counts/type labels only, never raw events or identifier values. Combined source limit 2,097,152 bytes. Omit in demo mode.

## `planJson` (type: `string`):

Own-input mode only: exact UTF-8 JSON string with schemaVersion analytics-taxonomy-plan/1, planVersion and events. Combined UTF-8 bytes of planJson and observationsJson must not exceed 2,097,152. Omit in demo mode.

## `useDemo` (type: `boolean`):

Use the included synthetic plan and observations. Omit planJson and observationsJson. Demo follows the same USD 0.10 completed-report pricing.

## Actor input object example

```json
{
  "authorizedNonpersonalNonconfidentialData": false,
  "useDemo": true
}
```

# Actor output Schema

## `results` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "useDemo": true
};

// Run the Actor and wait for it to finish
const run = await client.actor("immemorial_sarinda/analytics-taxonomy-drift").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "useDemo": True }

# Run the Actor and wait for it to finish
run = client.actor("immemorial_sarinda/analytics-taxonomy-drift").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "useDemo": true
}' |
apify call immemorial_sarinda/analytics-taxonomy-drift --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,immemorial_sarinda/analytics-taxonomy-drift"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/acA7Wv8nqgydSN60t/builds/1Qh73DC08K4g7FL5Y/openapi.json
