Telemetry and Privacy
Event types, sampling, the bulk endpoint, envelope fields, privacy levels, and client-side inspection — the pyvorin-native 1.0.9 telemetry system, precisely.
Published Aug 7, 2026
pyvorin-native 1.0.9 records seven kinds of event, sends them in batches to a single endpoint, and carries nothing in those batches but metadata. This page describes that system down to the field level: what each event is, how sampling decides what gets recorded, what the wire format looks like, which privacy levels exist, and exactly how to inspect the queue from your own process. Every behaviour quoted here was verified against the installed build, either by reading the telemetry core or by calling it live.
Two corrections to earlier documentation come first, because they change compliance decisions. Telemetry was never "raw source unless enabled" — no privacy level, flag or setting has ever caused source code to be collected by this package, and the phrase described an architecture that does not exist in it. And there is no pyvorin telemetry off command in either CLI; the opt-out is an in-process API call, described below.
The transport: batched, fire-and-forget
Telemetry uses a ring buffer capped at 1,000 events. There is no background thread. Events accumulate in memory until one of three triggers fires a flush: the buffer reaches 50 events, 300 seconds have passed since the last flush, or the process exits (an atexit hook drains whatever remains). Each flush sends one POST to https://api.pyvorin.com/v1/usage/events/bulk with a 5-second timeout. Failures are silently ignored — telemetry is explicitly designed so that losing it changes nothing about your program. Recording an event costs roughly a microsecond: an in-memory append under a lock, nothing more.
These properties are deliberate and worth understanding as a set. Fire-and-forget means the telemetry path can never raise into your code. No background thread means no scheduling surprises in latency-sensitive loops. Silent failure means a telemetry outage can never become your outage. The cost of that design is that delivery is not guaranteed: a process killed without atexit, or a network partition longer than the flush window, simply loses events. For a usage-analytics stream that is the right trade; for an audit log it would be disqualifying. Telemetry is not an audit log, and nothing on this page should be read as suggesting it can serve as one.
Event types and sampling
The event vocabulary is fixed and small. Verified from the telemetry core:
| Event | Recorded | Typical parameters |
|---|---|---|
compile | 1% sampled | Timing, such as {"time_ms": 1.0} |
execute | 1% sampled | Timing |
feature_used | 1% sampled | Feature name |
activation | Always | Activation outcome |
validation_failed | Always | Failure context |
tamper_detected | Always | Tamper context |
revoked | Always | Revocation context |
The sampling rule is applied at record time, per event, in the core itself: when an event named compile, execute or feature_used arrives, a hash of the event name plus the current time is taken modulo 100, and only one call in a hundred appends to the buffer. The security events bypass sampling entirely — they are the four that licensing and integrity subsystems emit when something is wrong, and a sampling scheme that could drop a tamper event would defeat their purpose. Dev mode records everything unsampled, which is one of several reasons PYVORIN_DEV_MODE=1 belongs nowhere near production.
The envelope: what a batch looks like
A captured batch, quoted verbatim from the runtime interception during verification:
{"v":"1.0","session":"9ad3dbb7-…","privacy":"standard","abuse_score":0,
"events":[{"t":…,"e":"compile","p":{"time_ms":1.0},"s":…}]}
Field by field, from the code that builds it:
| Field | Content |
|---|---|
v | Envelope version, currently "1.0" |
session | First 16 characters of a UUID4, generated once per process |
privacy | The active privacy level at flush time, as a string |
abuse_score | Integer computed locally; see below |
events[].t | Unix timestamp of the event |
events[].e | Event name, from the vocabulary above |
events[].p | Parameter dict — timings, feature names, outcome context |
events[].s | The session id, repeated per event |
What is not in the envelope deserves the same precision: no source code, no function names drawn from your codebase, no file paths, no argument or return values, no environment variables, no stack traces. The p field carries measurements and labels, not payloads. The session id correlates events within one process lifetime and nothing beyond it — a new process gets a new session, and there is no persistent cross-process identifier in the stream.
The abuse score is worth explaining because it sounds worse than it is. Three local counters feed it: validation_failures (incremented per licence-validation failure; contributes 1 once it exceeds 10), tamper_events (incremented per detected tamper; contributes 5 each), and compile_count (contributes 1 beyond 100,000 compiles in one process — a heuristic against abnormal hammering). All computation happens locally; the batch carries only the resulting integer. It is an anti-fraud tripwire, not a behavioural profile.
Privacy levels
Three levels exist, set per process:
| Level | Behaviour |
|---|---|
standard | Default. Sampled compile/execute/feature events plus all security events. |
minimal | Security events only: activation, validation_failed, tamper_detected, revoked. |
disabled | Nothing is recorded; every event is dropped at the door. |
The control surface is exactly one function, and it is the only opt-out that exists:
from pyvorin import _telemetry
_telemetry.set_privacy("minimal") # security events only
_telemetry.set_privacy("disabled") # no events recorded
print(_telemetry.get_privacy()) # confirm the active level
Verified live: the default in a fresh process is standard; after set_privacy("minimal") a recorded validation_failed event still queues while a compile event does not; after set_privacy("disabled") nothing queues at all. There is no environment variable, no config file, and no CLI subcommand for any of this. PYVORIN_TELEMETRY_URL overrides the destination URL, not the collection behaviour — setting it does not reduce what is recorded, only where it would go. One subtlety the live check exposed: set_privacy ignores unknown level strings rather than raising, so a typo in a level name silently leaves you at standard. Confirm with get_privacy() when it matters.
Where to put the call is a judgement call with a definite right answer. Set the privacy level in your application entrypoint, at start-up, before other pyvorin work; do not set it inside libraries, because a library that silently overrides the application's telemetry choice is making a policy decision it has no standing to make. If your requirement is fleet-wide silence, the code call above plus an egress block on the bulk endpoint gives you defence in depth with a verifiable backstop.
Inspecting the queue yourself
You do not have to take the envelope description on faith — the telemetry core exposes its own state, verified by calling it:
from pyvorin import _telemetry
print(_telemetry.get_diagnostics())
{'session_id': 'a68299d4-167c-48', 'privacy': 'standard',
'queued_events': 0, 'abuse_score': 0, 'abuse_counters': {}}
get_diagnostics() returns the session id, the active privacy level, the number of queued events, the current abuse score and the local abuse counters. It is a summary, not a payload dump: it tells you what would go, not the full content of each queued event. If you need the wire format itself, the honest client-side route is to redirect the flush to a sink you control with PYVORIN_TELEMETRY_URL and read the captured POST body — that is precisely how the envelope quoted above was obtained during verification. There is no telemetry inspect or telemetry preview command in either CLI, and we would rather tell you that than invent one.
Enterprise arrangements
The default configuration sends telemetry to Pyvorin's own endpoint under the standard privacy level. If your organisation needs a different arrangement — minimal or disabled as the enforced default across your fleet, a different destination, or questions about what the platform retains server-side — that is a conversation, not a setting. Contact us through the licensing dashboard at app.pyvorin.com, identify your licence key, and state the requirement. We do not publish retention figures we cannot evidence, and we will not claim a configuration exists when it does not; what we can do is agree arrangements explicitly, in writing, for your account.
Where to go next
- Security and source privacy — the full outbound surface, including the licence payload and what never leaves the machine.
- Network requirements — the bulk endpoint's place in the complete list, plus firewall guidance.
- Pyvorin GDPR considerations — the data-protection assessment view: personal data, retention posture, DPIA shape.
- Security best practices — telemetry in the context of hardening, including the environment-variable risks around redirection.
Last reviewed 7 August 2026 against pyvorin-native 1.0.9 installed at /root/pvfinal. Event vocabulary, sampling logic, envelope fields, privacy levels and get_diagnostics were read from the telemetry core and exercised with live calls; the quoted batch was captured by runtime interception. Server-side retention practices are outside what the artefact can prove and are not claimed here.