Generated banner for Rp Telemetry
Generated logo for Rp Telemetry

Rp Telemetry

WixOfficialPublished by Wix

  • Skill
  • Tool integration
  • Automation and integrations
  • MIT
  • Low risk
  • Has scripts

From the author

Always-active telemetry companion for RePlatform migration runs. Records what happened during a run — halts, errors, fidelity losses, API gaps, skill coverage gaps, user decisions, pipeline defects — through a validated recorder script, plus the run rollup (stages, timings, volumes, verification). Loaded by the orchestrator at run start and kept active for the whole run.
— the description by Wix

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

rp-telemetry is an always-active companion tool designed to capture structured telemetry data during RePlatform migration runs. It records observations about the migration process—including pipeline stages, API interactions, user decisions, and skill coverage gaps—without interfering with the migration logic itself. By providing a standardized way to log events and metrics, it enables data-driven improvements to migration skills and tooling. The tool enforces strict observational discipline, requiring that all recorded data be structural and free of sensitive information, ensuring that root-cause analysis can be performed reliably by reviewers after the run.

What it does

  • Records lifecycle events, stage boundaries, and run rollups for migration projects.
  • Captures performance metrics including latency, token usage, and API call statistics.
  • Logs observational data on errors, fidelity losses, and pipeline defects.
  • Tracks user decisions and manual interventions at critical checkpoints.
  • Identifies skill coverage gaps and missing API capabilities.
  • Provides a mechanism to finalize runs and push data to business intelligence sinks.

When to use it

  • At the start of every RePlatform migration run to ensure full observability.
  • When you need to track the performance of specific migration stages or API interactions.
  • When a migration run encounters an error, halt, or unexpected behavior that requires later analysis.
  • When you need to document user decisions or manual actions taken during the migration process.
  • When you need to report on the success or failure of entity migrations and verification spot-checks.

Advantages

  • Enables objective, data-driven analysis of migration performance and bottlenecks.
  • Separates observational data collection from migration execution logic.
  • Ensures consistent reporting across different migration runs and attempts.
  • Provides a structured way to identify and prioritize skill development needs.
  • Maintains privacy by scrubbing sensitive data from logs automatically.

Usage examples

“Start a new telemetry session for a migration project.”
Initializes the run tracking, stamps the skills version and commit, and prepares the recorder to capture subsequent events.
“Record a stage transition from discovery to mapping.”
Updates the telemetry log with the stage boundary, allowing for accurate measurement of active time spent in each phase.
“Log an API error encountered during the import stage.”
Captures the error code, stage, and context, enabling the team to identify and address the failure in future skill updates.
“Finalize the migration run with a summary of imported volumes.”
Generates a comprehensive rollup of the run, including entity counts, verification results, and operator acceptance status.

Install

  1. Download the skill as a ZIP:

    https://autogineer.com/agent-skills/wix/rp-telemetry-rqtn/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip rp-telemetry.zip -d .agents/skills

Documentation by Wix

rp-telemetry

Capture per-run telemetry that tells us what to improve in the skills and the underlying Wix tooling. This skill stays active alongside the migration skills for the entire run; the migration skills themselves are unchanged and know nothing about telemetry.

The one rule that governs everything you record

Observation, not diagnosis. Every field records what was observed — what happened, what was expected, what actually occurred, where, and how often. Never record a root cause, a fix, a recommendation, or a workaround-as-solution. There is deliberately no field for them; the schema rejects unknown fields. Root cause is derived later, at review time, by a reviewing agent with full context — not asserted by you in the moment.

Three discipline rules apply to every free-text field (what_happened, expected, actual):

  • Types, not instances. Refer to entity types and classes, never specific client data. entity_type: "product" — never a product's name, SKU, price, or body text.

  • Secret-safe. No credentials, tokens, URLs, config values, or file contents — the recorder also runs a mechanical scrub as a last line of defense, but do not rely on it.

  • Short and structural. A sentence or two, max 400 chars. Prefer stating expected-vs-actual over narrating. The coded fields carry the structure.

  • No remediation narration. "The canonical lib was fixed and re-synced" is a fix story, not an observation — even when true (dev-mode backports). Record what was observed to work ("adding fieldsets=FULL returned the field; the lookup succeeded") and leave the repair to the execution log.

The recorder

All telemetry is persisted through the bundled recorder — never hand-write or edit run-telemetry.json or telemetry/ files. Run it from this resource directory (see CONVENTIONS.md), pointing at the active migration project:

node scripts/rp-telemetry.js <command> [...] --project <abs path to migrations/<project>>

The recorder owns everything mechanical: schema validation (it rejects invalid enums and unknown fields — fix and retry, never guess around it), timestamps, per-class event folding, the run/attempt/session resume model, the privacy scrub, and the well-formedness gate at finalize. It prints one JSON result per call; {"ok":false,...} lists exactly what to fix.

CallWhen
start '<dims-json>'At run begin — before any other pipeline step. Resumes an unfinalized run automatically.
dims '<dims-json>'Whenever a dimension becomes known mid-run (platform version and extensions after discovery, site_id after provisioning).
stage start <stage> / stage end <stage> --outcome <outcome>At every stage boundary. Outcomes: passed, halted, failed, skipped.
meter [--api-ms N --model-ms N --script-ms N --input-tokens N …]Measured latency/token counts for a stage. Call it whenever you have real numbers; see "Metering" below.
wait start [--halt <subtype> --skill <s> [--what '<text>']] / wait endThe moment the run halts for the user, and the moment it resumes. This is how user latency stays out of active_ms — never estimate elapsed time yourself. Pass --halt for a needs-user halt and the recorder emits the paired halt_needs_user event for you.
record '<event-json>'The moment something observable happens (taxonomy below).
digest --transcript <path> [--offline]Just before finalize — a deterministic parse of this run's Claude Code session transcript (cost, turn counts, tool failures, retry loops, hook errors). Never authored by you; --offline re-runs it later against a checkpointed or local transcript for a run that died before calling it.
finalize '<rollup-json>'When the run reaches a terminal state.
rebuild [--attempt <n>] [--push]Only on request, to re-assemble a past run's signal document from its archived journal after a recorder fix — never during a run. --push re-emits the rebuilt run to the BI sink (backfill after an outage; idempotent — the reviewer dedupes at query time).
statusTo orient after a resume.
Run lifecycle
  1. start as the first telemetry act of the run, with whatever dimensions are already known:

    node scripts/rp-telemetry.js start '{"source_platform":"wordpress",
      "source_site_url":"https://client-site.example","source_acquisition":"public_storefront",
      "delivery_mode":"management","destination_strategy":"new_site",
      "runtime_env":{"agent_runtime":"claude-code","model":"<model id>"}}' --project <dir>

    start on a project with an unfinalized run resumes it (same run, same attempt, one more session) and closes any open wait interval. start after a finalized run opens the next attempt. Never try to manage run identity yourself.

    Always start on resume, even mid-stage. Besides the session accounting, the resume is a BI heal point: the recorder re-pushes everything the previous session's transport may have lost (push failures are journaled, then healed at the next opportunity — a halted run may never reach finalize, so skipping the resume start can strand the run's tail in the local journal). status shows bi_backlog: true when unpushed rows are pending.

    source_acquisition is an open set of class tokens, but reuse an established one (admin_api, public_storefront, public_content, file_export) rather than minting a synonym — cross-run folding depends on stable tokens.

    Skills provenance is auto-stamped — you don't pass it. At start the recorder stamps two identifiers onto the run so every record can be traced back to the skills that produced it:

    • skills_version — the bundle's semver release label (from VERSION). Coarse and hand-bumped by design; it is the group-by/order-by key that binds issues to a release line ("all issues on 1.2.x", "regressed since 1.1.0").

    • skills_commit — the exact source commit the bundle was built from: the precise vendored snapshot within a release. Many commits ship under one hand-bumped skills_version, so a run reporting 1.0.0 is otherwise unattributable to the change that produced its issue. The recorder resolves it, in order of authority, from the sourceCommit stamped into .publish-manifest.json at publish time (the only source that works in a partner runtime, and the only correct one once the bundle is vendored into wix/skills), else a dev-mode git rev-parse --short HEAD of the source checkout, else null.

    Both may be overridden by passing skills_version / skills_commit in the start dims (a runtime with better provenance than the recorder can infer), but normally you leave them to auto-resolve.

  2. Stage boundaries as the pipeline moves. Map orchestrator steps to stages like this:

    StageCovers
    configEverything before discovery: project resolution, config files, up-front input collection
    discoverySource discovery (rp-discovery + source adapter)
    mcp_gateThe Wix MCP prerequisite gate between discovery and mapping
    mappingrp-mapper producing the mapping plan
    mapping_reviewThe mapping review checkpoint (user-facing)
    setup_discoveryrp-setup-discovery
    codegenrp-import-codegen
    approval_gateThe execution-plan approval gate (user-facing)
    setup_provisioningSite creation, app installs, collections — rp-execute-setup
    storefront_buildThe website-mode wix-headless build + release; skipped in management mode
    extractSource extraction to disk, before any write (rp-execute-import)
    importThe import writes (rp-execute-import)
    finishVerification spot-checks, completion reporting, handoff

    Stages bind to when the work actually runs, not to their canonical order. If the orchestration runs extraction early (e.g. extract + dry-run before the approval gate, inside the codegen phase), close the open stage, bracket the extraction in its own extract stage, then reopen — a stage may be entered more than once, and its active_ms sums the entries. Booking a real extraction into codegen (and leaving extract as a milliseconds-long token stage) is exactly the mis-attribution the extract stage exists to prevent.

    When the discovery stage ends, always stamp what it learned: dims '{"source_platform_version":"...","source_extensions":["..."], "discovered_entity_types":["post","product","event"]}' — pass [] explicitly when discovery found no extensions (or, improbably, no entity types); a null left behind is flagged in telemetry_health as source_extensions_null_after_discovery / discovered_entity_types_null_after_discovery. discovered_entity_types lists every source entity class discovery found in use — including classes with no Wix target; it is what finalize cross-checks the volumes rows against, so the "what we couldn't import" demand signal cannot be silently under-reported.

  3. wait start / wait end around every needs-user halt: credential requests, the mapping-review checkpoint, the approval gate, any halt-to-needs-user. Pass the halt class on the same call — wait start --halt missing_input --skill rp-mapper --what "run halted at the mapping-review checkpoint awaiting approval" — and the recorder emits the paired halt_needs_user event mechanically; a stalled wait with no halt event in its stage is flagged in telemetry_health (wait_without_halt_event:<stage>). If the session is about to end on a halt, leave the wait open — the resume's start closes it, so overnight user latency lands in waiting_ms where it belongs. If you resume real work while a wait is open (e.g. investigating something before the user has answered), wait end first and wait start again when you go back to waiting — active work must never be booked as waiting.

  4. record events as they happen (next section). Record in the moment, not retrospectively — improvisation and halts are only reliably knowable when they occur.

  5. digest --transcript <path to this session's CC transcript> just before finalize. It parses the Claude Code session transcript, deterministically — no model, no judgment, nothing for you to say. If it fails, carry on to finalize anyway; telemetry never blocks a migration.

  6. finalize once, at a terminal state, with the rollup:

    node scripts/rp-telemetry.js finalize '{"terminal_state":"completed",
      "volumes":[{"entity_type":"product","target":"native","target_surface":"stores/v3",
        "discovered":142,"planned":142,"attempted":142,"succeeded":139,"failed":3,
        "skipped":0,"already_imported":0}],
      "verification":[{"subject":"product","method":"query_back","checked":10,"passed":10,"failed":0}],
      "operator_acceptance":"accepted"}' --project <dir>
    • volumes come from the execution artifacts (manifest, audit log, crosswalk): planned and target/target_surface from the approved plan; already_imported is crosswalk-skipped scope from a prior attempt — never fold it into skipped.

    • Include a volume row for every entity type discovery found in use — including types excluded by user decision or with no clean Wix target (discovered: N, skipped: N, target: none where no target exists, planned 0 or null). "What we cannot or chose not to import" must be signal-layer arithmetic, never an evidence dig — rows only for imported types silently erase the excluded half of the approved plan. This is enforced mechanically: on a completed run, any class stamped in discovered_entity_types with no volume row is flagged volume_row_missing:<type> in telemetry_health.

    • verification makes the finish step's spot-checks countable. checked: 0 is an honest "written but unverified" — report it rather than papering over it.

    • Do not finalize a halt you expect the user to resume — leave the run unfinalized with the wait open. Finalize with halted_needs_user only when the run is genuinely being closed out in a stalled state. A user who declines the plan is abandoned_by_user (plus a user_decision event with subtype: declined), never halted_needs_user or failed.

    • Check the returned bi_push. If it reports failed > 0 or skipped > 0, the run (or part of it) did not reach the central BI record: retry once with rebuild --push, and if it still fails, tell the operator in the completion report that the run exists only locally — never let a run silently vanish from the shared record. skipped > 0 means RP_TELEMETRY_BI_DISABLED was set during the session; on a real migration run that env var must never be set. A clean bi_push result (failed: 0, skipped: 0) means the rows were handed to frog, not that they have arrived in BI — per bi-sink.js's own transport code, a 2xx is not an ingestion receipt; only a Trino read-back proves arrival, and no such read-back is available from inside a run. Report bi_push success as "handed off," never as "confirmed in BI."

Metering — splitting active_ms into where the time actually went

active_ms alone cannot locate a bottleneck: it is wall-clock between two stage boundaries, fusing model reasoning + subprocess execution + remote API latency + defect-repair time into one number. meter splits it, and cost is derived from the token counts.

# a generated script reporting its own measured work
node scripts/rp-telemetry.js meter --api-ms 250000 --api-calls 73 --api-retries 2 --script-ms 1200 --project <dir>
# the agent runtime reporting its own usage for the stage it just finished
node scripts/rp-telemetry.js meter --model-ms 42000 --input-tokens 8000 --output-tokens 1500 --cache-read-tokens 120000 --project <dir>

Derive the API half mechanically — do not hand-count it. After a generated import runs:

node scripts/meter-from-audit.js --project <migration dir> --stage import   # --dry-run to preview

It reads logs/import-audit.ndjson and emits measured api_ms / api_calls / api_retries. It exists because a bulk write logs one audit row per item, each carrying the batch's total latency — summing rows multiplies one call's latency by its item count, which on a real run reported 1,050,992 ms of API time inside a stage that only existed for 346,505 ms. The helper collapses rows per distinct (runId, endpoint, batch, latencyMs), so a batch counts once while genuinely separate calls each count.

Fields: durations model_ms / api_ms / script_ms; counts input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, api_calls, api_retries. Defaults to the open stage; --stage <stage> targets another. Repeated calls accumulate, so each script invocation and each stage re-entry reports independently.

Rules that keep the numbers trustworthy:

  • Measure, never estimate. Every field is a number something actually observed — a script's own elapsed time, an audit log's summed latencyMs, the runtime's reported usage. If you do not have a measurement, omit the field; the gap is reported as unattributed_ms and flagged, which is far more useful than a guess.

  • Do not meter the same interval twice. Meters sum, so re-reporting a stage's API time after a resume double-counts it. The recorder flags stage_over_attributed:<stage> when attributed time exceeds elapsed time, because unattributed_ms clamps at zero and would otherwise hide it.

  • Cost is derived, never recorded. Pass model_pricing_snapshot in the start dims ({ "<model>": { "input_per_mtok": 15, "output_per_mtok": 75, "cache_read_per_mtok": 1.5 } }) and the rollup computes cost.estimated_cost_usd. A stored dollar figure silently goes wrong when list prices change; tokens plus a dated snapshot stay recomputable. Without a snapshot, cost is null with cost_basis: "no_pricing_snapshot" — never a fabricated number.

  • contained_recovery is derived, not self-reported. Any error or pipeline_defect in a stage marks it, because a stage that spent its time debugging is the one an agent is least likely to remember to flag. timing.stages_with_recovery lists them, so a clean run and a thrash are distinguishable instead of both reading as "this is what the stage costs".

The rollup's timing.agentic_ms vs timing.deterministic_ms (and agentic_share) is the number that shows whether moving work into deterministic code is paying off. agentic_share is null when nothing was metered rather than 0, which would falsely read as a fully deterministic run.

Collecting operator_acceptance

At the finish handoff, ask the operator one plain question: does the migrated result look right to them — accepted, rework_needed, or rejected? Record their answer in finalize. If the run never reaches finish or they do not answer, it stays unknown. This is the only field separating "completed and good" from "completed and unusable" — ask it, but never pressure or interpret; their verdict as given, coarse by design.

Event taxonomy — when to record what

Events are folded per problem class by the recorder (same type + stage + entity type + API surfaces + app + error code + subtype fold into one event with a count), so record every occurrence class once and pass count when you observed many at once — e.g. a generated script reporting 4,000 identical write failures is one record call with "count": 4000. Distinct problems are distinct subtypes or error codes, not bigger counts.

Every event needs: event_type, stage (defaults to the open stage), skill (the active rp-* resource, e.g. "rp-mapper"), severity (blocking | degraded | cosmetic | info), and what_happened. Add entity_type, wix_api_surface (e.g. stores/v3), source_api_surface (endpoint class like wp/v2/posts — never a URL), wix_app_id, and error_code whenever they apply — they are the cross-run group-by keys.

  1. halt_needs_user — the run stopped at a defined needs-user state. Prefer the mechanical form — wait start --halt <subtype> … emits it for you (lifecycle step 3); record it yourself only for a halt with no wait interval. subtype: missing_input (missing/invalid required input or credential) | manual_only (genuinely manual step, no API) | systemic_failure (systemic failure or data-loss risk).

  2. manual_action_required — the execution plan flagged something as "can't be done, needs manual action" (e.g. a storage-plan upgrade), whether or not the run halted on it. subtype: plan_or_billing | dashboard_only | external_dependency | other.

  3. error — an API or script error, including recovered ones. Carry error_code (required — it is the discriminator; no subtype), retry_count, and recovered. Record one event per resolved retry chain, after the outcome is known — the final recovered, the total retry_count — never one call per attempt (per-attempt calls fold into one class, and the failing first attempt's fields would bury the recovery). When recovery required a change (not just a retry), actual must record what change made it succeed, as an observation: "retried with the description as plain text instead of HTML; the write succeeded" — never "the fix is X". If that change deviated from the documented path — including a change to a planned command or value, like simplifying an input the plan specified — also emit a paired skill_coverage_gap (undocumented_workaround).

  4. fidelity_loss — the migration technically proceeded but lost something. subtype: dropped_field | unverified_enum | no_target (source entity with no clean Wix target) | coerced_value. For a loss triggered by specific records, inline the offending record's sanitized shape in observed_shapes — field names and types and which field was dropped/coerced, never values.

  5. api_gap — a missing or insufficient Wix API capability. subtype: missing_api | missing_capability (API exists but cannot express the operation) | internal_only (capability exists but is not publicly exposed) | other. Carry wix_api_surface (required) and error_code — that signature is matched centrally across runs. Never write to any backlog file; the event is the whole per-run obligation.

  6. skill_coverage_gap — the standing self-report, and the highest-value signal for improving the skills. Whenever you act beyond what the active skill explicitly told you to do — you guess a value, work around a missing instruction, resolve an ambiguity by judgment, or handle a case the skill does not cover — record it at that moment. subtype: guessed_value | undocumented_workaround | ambiguous_instruction | path_not_covered. Describe the situation and the action taken as an observation: "skill did not specify which enum value maps to X, so a value was chosen to proceed" — never "the skill should add Y". If you are unsure whether something counts as improvisation, it does — record it.

  7. user_decision — the user's answer at every defined checkpoint or fork: the mapping-review checkpoint, the approval gate, and intake forks (comments anonymization, delivery mode, media reachability, …). subtype: accepted | declined | deferred | amended; decision_point (required) names the fork, e.g. mapping_review, approval_gate, comments_anonymization. Record the decision, not the user's reasoning verbatim. An amended acceptance whose change exposed a mapping problem additionally surfaces that problem as fidelity_loss or skill_coverage_gap. A declined at a terminal checkpoint pairs with terminal_state: abandoned_by_user.

  8. pipeline_defect — our own migration machinery misbehaved: not a Wix API error, not a source failure, not your improvisation. subtype: state_inconsistency (two pipeline state artifacts disagree) | ordering_violation (a step ran before its prerequisite's output existed) | record_defect (a pipeline record/log is malformed or misleading as evidence) | other. Name the artifacts and their states in what_happened — names and states, never contents.

  9. source_plugin_coverage — which of the source site's plugins we could not resolve, plus one run-level summary. Record after discovery writes discovery/plugin-coverage.json.

    Record one event per unresolved item only. The unresolved set is: every pending row (including reason: cannot-tell and unread candidate namespaces), every requires-development row, and every row carrying blocked[] entries (a user-file / credential / surface-changed blocker, declined or not). These are the whole point of this event type — they are the only signal that says which plugin profile to write next, and without them that ordering is guesswork.

    Do not emit a per-row event for anything already resolved: migration-planned rows with confidence: confirmed, or no-need-to-migrate rows with basis: list or basis: decision (a human already signed that verdict). On a 55-plugin store that would be ~29 events restating a checked-in list — telemetry with no information content, which makes the real backlog harder to see, not easier. (A migration-planned or no-need-to-migrate row that automation derived — confidence: proposed / basis: proposed — is reported through skill_coverage_gap (guessed_value), not here.)

    Emit exactly one aggregate event per run carrying the distribution: plugins installed, counts by the four statuses (and by via / confidence for planned rows), blocked counts by kind, and whether the plugin list was available. That preserves the cross-run view of how coverage is trending without one event per plugin.

    Per-event fields: subtype is the row's status (pending | requires_development) or blocked for a blocker entry (then also carry the blocker kind); carry the plugin slug and version, the capability, the read channel, whether the row was recognized, and source_api_surface as the endpoint class. Slug and version are required — without them a cross-run view cannot tell which plugin caused what. Plugin slugs and versions are software identifiers, not client data; still never record site URLs or record contents.

    A requires-development row additionally pairs with api_gap, and a lossy via: cms row with fidelity_loss — this event reports distribution and backlog, never the gap itself.

evidence_refs — rare, not routine

Author every event to be self-sufficient: coded fields + bounded free text + observed_shapes should let a reviewer triage it without opening any artifact. Add evidence_refs ({"artifact":"execution-log.md","locator":"## Import"}) only for the exceptional event whose signal genuinely cannot carry the full observation. Use project-root-relative paths and prefer section headings as locators (line ranges break when files regenerate). Never reference secret-bearing config files.

What never to do

  • Never hand-write, edit, or re-read run-telemetry.json or telemetry/ files — the recorder appends; you only call it.

  • Never record client data values, names, URLs beyond the recorded source origin, or secrets — in any field, including shapes and locators.

  • Never record a fix, root cause, or recommendation — observations only.

  • Never estimate durations — timing comes from stage/wait boundary calls.

  • Never skip a rejected call: fix the listed fields and retry. Rejections are counted against telemetry health either way.

  • Never create or update improvement/feature-request backlog files during a run — those are maintained centrally from many runs' telemetry, not per run.

  • Never delete or trim project artifacts for telemetry reasons — capture is a non-destructive view; size is not a reason to drop anything.

Files

lib/bi-sink.js

'use strict';

// Wix BI sink for the telemetry recorder — the one transport path.
//
// Wire format (proven end-to-end 2026-07-26):
// POST https://frog.wix.com/migration-data-validator with a JSON batch envelope
//   {"dt":0,"g":{"src":10,"evid":<evid>},"e":[{"dt":0,"f":{...fields}}]}
// capped at 100 events / ~240KB per request. No transport auth — identity is the
// claim-based `logged_user_id` field, routing every row to events.dbo.users_10.
//
// Three hard rules, each a silent-drop trap (2xx with no row, or NULL columns):
//   1. Schema fields go up in camelCase (`runId`, `errorType`, ...) — the catcher
//      matches registered param names, not the snake_case Trino column names.
//      Standard identity fields stay snake_case (`logged_user_id`).
//   2. Every GUID-typed field must be a real GUID; a non-hex value fails
//      validation before routing and drops the whole event.
//   3. Every row carries `logged_user_id` = the operator's wix_user_id (GUID).
//
// frog's HTTP status is not an ingestion receipt — only a Trino read-back proves
// arrival. The recorder treats any non-2xx or network error as a push failure
// (journaled into telemetry_health); a 2xx merely means "handed to frog".
//
// Env switches (RP_TELEMETRY_BI_DISABLED is also the user opt-out documented in README.md):
//   RP_TELEMETRY_BI_ENDPOINT   override the frog endpoint (contract-test mock)
//   RP_TELEMETRY_BI_DISABLED=1 skip all pushes entirely

const SRC = 10;
const EVID_RUN = 5012; // replatform_run — run-grain lifecycle, phase started|finalized
const EVID_RUN_EVENT = 5013; // replatform_run_event — one row per observation class
const DEFAULT_ENDPOINT = 'https://frog.wix.com/migration-data-validator';
const MAX_EVENTS_PER_REQUEST = 100;
const MAX_BODY_BYTES = 240 * 1024;
const PUSH_TIMEOUT_MS = 8000;

const GUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;

function isGuid(value) {
  return typeof value === 'string' && GUID_RE.test(value);
}

function disabled() {
  return process.env.RP_TELEMETRY_BI_DISABLED === '1';
}

function endpoint() {
  return process.env.RP_TELEMETRY_BI_ENDPOINT || DEFAULT_ENDPOINT;
}

// Empty-value convention: unknown fields are omitted, never sent as null/"".
// JSON-typed schema fields (arrays/objects) are JSON-serialized into their value.
// GUID-typed fields are dropped unless they hold a real GUID (hard rule 2 —
// sending a non-GUID would silently drop the whole event, not just the field).
function put(fields, key, value, { json = false, guid = false } = {}) {
  if (value === undefined || value === null) return;
  if (guid && !isGuid(value)) return;
  fields[key] = json ? JSON.stringify(value) : value;
}

// `replatform_run` phase:started — built from the journal's run_start record, so
// its content is a pure function of run start and re-pushes are byte-identical
// (dedup key: (run_id, phase), latest row wins). `wixUserId` is the one late
// addition: identity may arrive via a later dims call, and the row is only
// sendable once it has (rule 3), so it rides along when known.
function startedRunRow(runStart, wixUserId) {
  const dims = runStart.dims || {};
  const f = { phase: 'started' };
  put(f, 'runId', runStart.run_id, { guid: true });
  put(f, 'projectId', runStart.project_id);
  put(f, 'attempt', runStart.attempt);
  put(f, 'sessionCount', 1);
  put(f, 'schemaVersions', runStart.schema_version);
  put(f, 'skillsVersion', runStart.skills_version);
  put(f, 'runtimeEnv', runStart.runtime_env, { json: true });
  put(f, 'wixUserId', wixUserId, { guid: true });
  put(f, 'sourcePlatform', dims.source_platform);
  put(f, 'sourcePlatformVersion', dims.source_platform_version);
  put(f, 'sourceExtensions', dims.source_extensions, { json: true });
  put(f, 'sourceSiteUrl', dims.source_site_url);
  put(f, 'sourceAcquisition', dims.source_acquisition);
  put(f, 'deliveryMode', dims.delivery_mode);
  put(f, 'destinationStrategy', dims.destination_strategy);
  put(f, 'runStarted', runStart.ts);
  return { evid: EVID_RUN, fields: f };
}

// `replatform_run` phase:finalized — the complete rollup. Local names map to the
// as-registered BI names here and nowhere else (see the Registration notes):
// schema_version→schemaVersions, verification→verificationType. skills_commit and
// discovered_entity_types are local-only (not registered BI columns) and
// deliberately not sent — the entity-class demand signal reaches BI through the
// `volumes` column; the dim exists for the finalize cross-check, whose flags
// travel in telemetryHealth.
function finalizedRunRow(rollup) {
  const f = { phase: 'finalized' };
  put(f, 'runId', rollup.run_id, { guid: true });
  put(f, 'projectId', rollup.project_id);
  put(f, 'attempt', rollup.attempt);
  put(f, 'sessionCount', rollup.session_count);
  put(f, 'schemaVersions', rollup.schema_version);
  put(f, 'skillsVersion', rollup.skills_version);
  put(f, 'runtimeEnv', rollup.runtime_env, { json: true });
  put(f, 'wixUserId', rollup.wix_user_id, { guid: true });
  put(f, 'siteId', rollup.site_id, { guid: true });
  put(f, 'sourcePlatform', rollup.source_platform);
  put(f, 'sourcePlatformVersion', rollup.source_platform_version);
  put(f, 'sourceExtensions', rollup.source_extensions, { json: true });
  put(f, 'sourceSiteUrl', rollup.source_site_url);
  put(f, 'sourceAcquisition', rollup.source_acquisition);
  put(f, 'deliveryMode', rollup.delivery_mode);
  put(f, 'destinationStrategy', rollup.destination_strategy);
  put(f, 'terminalState', rollup.terminal_state);
  put(f, 'stoppedAtStage', rollup.stopped_at_stage);
  put(f, 'stages', rollup.stages, { json: true });
  put(f, 'volumes', rollup.volumes, { json: true });
  put(f, 'verificationType', rollup.verification, { json: true });
  put(f, 'operatorAcceptance', rollup.operator_acceptance);
  put(f, 'telemetryHealth', rollup.telemetry_health, { json: true });
  put(f, 'eventCountByType', rollup.event_count_by_type, { json: true });
  put(f, 'activeMs', rollup.active_ms);
  put(f, 'waitingMs', rollup.waiting_ms);
  put(f, 'runStarted', rollup.run_started);
  put(f, 'runEnded', rollup.run_ended);
  put(f, 'bundleManifest', rollup.bundle_manifest, { json: true });
  // spec 0039 §5: the transcript_digest is a run-grain fact, so it rides the
  // existing finalized row rather than opening a new BI call — one JSON field,
  // matching how `stages`/`volumes` already travel, rather than one registered
  // column per sub-field. A run with no digest (offline recompute skipped, or
  // a run that predates 1.5.0) simply omits the field, same as any unknown.
  //
  // Known limitation (verified live, spec 0039 §5): as of 2026-08-17, `bi_events_raw`
  // shows evid 5012 rows with an EMPTY dimensions/measures map for every one of them —
  // this field will land alongside `terminalState`/`volumes`/every other field already
  // on this row and be equally unqueryable until whoever owns the BI schema resolves
  // why 5012/5013 aren't materializing their payload. Nothing to fix here: this `put()`
  // call matches the pattern every other field on the row already uses correctly.
  put(f, 'transcriptDigest', rollup.transcript_digest, { json: true });
  return { evid: EVID_RUN, fields: f };
}

// `replatform_run_event` — one row per folded observation class. Re-pushing a
// class after more occurrences folded in is by design: the dedup key is
// (run_id, seq) and the latest row carries the cumulative count. Renames:
// event_type→eventTypeName, error_code→errorType, expected/actual→
// expectedSkill/actualSkill.
function eventRow(folded) {
  const f = {};
  put(f, 'runId', folded.run_id, { guid: true });
  put(f, 'seq', folded.seq);
  put(f, 'count', folded.count);
  put(f, 'stage', folded.stage);
  put(f, 'skill', folded.skill);
  put(f, 'eventTypeName', folded.event_type);
  put(f, 'subtype', folded.subtype);
  put(f, 'entityType', folded.entity_type);
  put(f, 'severity', folded.severity);
  put(f, 'wixApiSurface', folded.wix_api_surface);
  put(f, 'sourceApiSurface', folded.source_api_surface);
  put(f, 'wixAppId', folded.wix_app_id, { guid: true });
  put(f, 'errorType', folded.error_code);
  put(f, 'decisionPoint', folded.decision_point);
  put(f, 'retryCount', folded.retry_count);
  put(f, 'recovered', folded.recovered);
  put(f, 'whatHappened', folded.what_happened);
  put(f, 'expectedSkill', folded.expected);
  put(f, 'actualSkill', folded.actual);
  put(f, 'observedShapes', folded.observed_shapes, { json: true });
  put(f, 'shapesSeen', folded.shapes_seen);
  put(f, 'evidenceRefs', folded.evidence_refs, { json: true });
  return { evid: EVID_RUN_EVENT, fields: f };
}

function envelopeBody(evid, events) {
  return JSON.stringify({ dt: 0, g: { src: SRC, evid }, e: events });
}

// A single event over the body cap sheds its unbounded-ish JSON fields before
// being declared unsendable. Truncation is surfaced (telemetry_health flag),
// never silent.
const SHEDDABLE_FIELDS = ['observedShapes', 'evidenceRefs', 'bundleManifest', 'stages', 'volumes'];

function shedOversize(event) {
  const f = { ...event.f };
  let shed = false;
  for (const key of SHEDDABLE_FIELDS) {
    if (f[key] === undefined) continue;
    delete f[key];
    shed = true;
  }
  return shed ? { ...event, f } : null;
}

async function sendBatch(evid, events) {
  const body = envelopeBody(evid, events);
  try {
    const res = await fetch(endpoint(), {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body,
      signal: AbortSignal.timeout(PUSH_TIMEOUT_MS),
    });
    if (!res.ok) return `http_${res.status}`;
    return null;
  } catch (error) {
    return error && error.name === 'TimeoutError' ? 'timeout' : 'network_error';
  }
}

// Push rows (from the builders above) as the given operator. Never throws:
// returns { sent, failures: [{evid, rows, detail}], truncated, skipped } and
// the caller journals the failures — telemetry must never block a migration.
// A disabled sink reports the rows it swallowed in `skipped` instead of
// masquerading as a successful push — the caller journals those too, so a
// kill-switch leaking into a production session is visible, never silent.
async function pushRows(rows, loggedUserId) {
  const out = { sent: 0, failures: [], truncated: 0, skipped: 0 };
  if (rows.length === 0) return out;
  if (disabled()) {
    out.skipped = rows.length;
    return out;
  }
  if (!isGuid(loggedUserId)) {
    out.failures.push({ evid: null, rows: rows.length, detail: 'no_operator_identity' });
    return out;
  }
  const byEvid = new Map();
  for (const row of rows) {
    if (!byEvid.has(row.evid)) byEvid.set(row.evid, []);
    byEvid.get(row.evid).push({ dt: 0, f: { ...row.fields, logged_user_id: loggedUserId } });
  }
  for (const [evid, events] of byEvid) {
    const queue = [events];
    while (queue.length > 0) {
      let chunk = queue.shift();
      if (chunk.length > MAX_EVENTS_PER_REQUEST) {
        const mid = Math.ceil(chunk.length / 2);
        queue.unshift(chunk.slice(0, mid), chunk.slice(mid));
        continue;
      }
      if (Buffer.byteLength(envelopeBody(evid, chunk)) > MAX_BODY_BYTES) {
        if (chunk.length > 1) {
          const mid = Math.ceil(chunk.length / 2);
          queue.unshift(chunk.slice(0, mid), chunk.slice(mid));
          continue;
        }
        const slimmed = shedOversize(chunk[0]);
        if (!slimmed || Buffer.byteLength(envelopeBody(evid, [slimmed])) > MAX_BODY_BYTES) {
          out.failures.push({ evid, rows: 1, detail: 'oversized_event' });
          continue;
        }
        out.truncated += 1;
        chunk = [slimmed];
      }
      const failureDetail = await sendBatch(evid, chunk);
      if (failureDetail) out.failures.push({ evid, rows: chunk.length, detail: failureDetail });
      else out.sent += chunk.length;
    }
  }
  return out;
}

module.exports = {
  SRC,
  EVID_RUN,
  EVID_RUN_EVENT,
  MAX_EVENTS_PER_REQUEST,
  MAX_BODY_BYTES,
  isGuid,
  disabled,
  startedRunRow,
  finalizedRunRow,
  eventRow,
  pushRows,
};

Requirements and compatibility

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Needs: Needs Node.js

Security

What our scanner noticed· Low risk

  • Scripts that your agent may run

This skill includes scripts that your agent may run. Read them before you use it.

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Published by
Wix
Publisher
Wix (Official)
Source
wix/skills · wix-replatform/resources/rp-telemetry · commit d168bc2
Licence
MIT License (redistribution allowed)
  • LICENSE (added by AutoGineer from LICENSE)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by Wix or Wix.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download rp-telemetry.zip