Analytics Taxonomy Drift avatar

Analytics Taxonomy Drift

Pricing

$100.00 / 1,000 completed reports

Go to Apify Store
Analytics Taxonomy Drift

Analytics Taxonomy Drift

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.

Pricing

$100.00 / 1,000 completed reports

Rating

0.0

(0)

Developer

Picknotes

Picknotes

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

4 days ago

Last modified

Categories

Share

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

{"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:

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

Save this as observations.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:

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.

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:

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.