paperclipai/paperclip · warning

dropping 1 event with a non-serializable dimension (circular

Error message

dropping 1 event with a non-serializable dimension (circular reference?); event="${chunk[0]?.name}"

What it means

While size-splitting a batch, the serialized byte count for a single-event envelope is not a finite number — classically because JSON.stringify failed or degenerated on a circular reference inside an event dimension. The event is dropped with this warning and never reaches the collector.

Source

Thrown at packages/shared/src/telemetry/client.ts:190

   */
  private chunkForSend(events: TelemetryEvent[]): TelemetryEvent[][] {
    const maxCount = Math.max(1, this.caps.maxEventsPerBatch);
    const out: TelemetryEvent[][] = [];
    for (let i = 0; i < events.length; i += maxCount) {
      this.splitByBytes(events.slice(i, i + maxCount), out);
    }
    return out;
  }

  private splitByBytes(chunk: TelemetryEvent[], out: TelemetryEvent[][]): void {
    if (chunk.length === 0) return;
    const bytes = this.serializedBytes(this.buildEnvelope(chunk));
    if (bytes <= this.caps.maxBodyBytes) {
      out.push(chunk);
      return;
    }
    if (chunk.length === 1) {
      this.warn(
        Number.isFinite(bytes)
          ? `dropping 1 event whose serialized envelope exceeds maxBodyBytes (${this.caps.maxBodyBytes} bytes); event="${chunk[0]?.name}"`
          : `dropping 1 event with a non-serializable dimension (circular reference?); event="${chunk[0]?.name}"`,
      );
      return;
    }
    const mid = Math.ceil(chunk.length / 2);
    this.splitByBytes(chunk.slice(0, mid), out);
    this.splitByBytes(chunk.slice(mid), out);
  }

  private buildEnvelope(events: TelemetryEvent[], batchId?: string): TelemetryEventEnvelope {
    const state = this.getState();
    return {
      app: this.config.app ?? "paperclip",
      schemaVersion: this.config.schemaVersion ?? "1",
      installId: state.installId,
      version: this.version,

View on GitHub (pinned to 120ae5428f)

Solutions

  1. Find the emit site for the named event and replace the raw object with a flat, explicitly-selected DTO.
  2. If the object must go through, run it through a JSON-safe replacer that drops circular keys before emit.
  3. Add a dev-mode assertion that JSON.stringify(event) succeeds for every emit.
  4. Prefer primitive fields (ids, names, counts) over nested objects in dimensions.

Example fix

// before
telemetry.emit({ name: 'task_inspected', dims: { task } });

// after
telemetry.emit({ name: 'task_inspected', dims: { taskId: task.id, status: task.status } });
Defensive patterns

Strategy: validation

Validate before calling

function isJsonSafe(value: unknown, seen = new WeakSet()): boolean {
  if (value === null || typeof value !== 'object') return true;
  if (seen.has(value as object)) return false;
  seen.add(value as object);
  return Object.values(value as object).every((v) => isJsonSafe(v, seen));
}

// before emit:
if (!isJsonSafe(event)) throw new Error(`Circular reference in telemetry event ${event.name}`);

Type guard

function isSerializableTelemetryEvent(e: TelemetryEvent): boolean {
  try {
    return Number.isFinite(Buffer.byteLength(JSON.stringify(e)));
  } catch {
    return false;
  }
}

Prevention

When it happens

Trigger: A telemetry dimension contains an object graph with a cycle: parent↔child associations, self-referential error.cause chains, ORM rows with relations, AST/scope objects captured wholesale.

Common situations: Passing live domain objects instead of DTOs; logging `error.cause` chains that loop; serializing request/response objects with back-references; reusing parsed structures that reference their container.

Related errors


AI-assisted analysis of paperclipai/paperclip@120ae5428f (2026-08-18). Data as JSON: /api/errors/5b48294583649479. Report an issue: GitHub.