DocsMigrations

Migrations

Migrate from Heap

5 min readReviewed July 2026

Heap combines autocaptured interaction data with labeled events, sessions, users, and custom events. A useful GraphJSON migration should preserve the business events you rely on, not reproduce every captured click and DOM detail without a decision it supports.

Build a target tracking plan first. Then import only the history needed for cohorts, comparisons, customer support, or continuity.

Choose an export path#

Heap documents two relevant paths:

  • Heap Connect can export selected labeled data to S3, Redshift, BigQuery, or Snowflake.
  • Heap also documents a one-time CSV export when leaving the product.

Use Heap Connect when you already have access and need:

  • repeatable extraction
  • warehouse-side filtering and transformation
  • users and sessions beside events
  • a pilot followed by a larger import

Use a one-time export when it is the available portability path. Confirm its date range, selected events, format, and identity fields before planning the cutover.

Export availability depends on the Heap subscription and configuration. Do not promise a migration window until a sample file or table has been inspected.

Inventory labeled and custom events#

Create a mapping worksheet:

Heap source GraphJSON event Keep history? Target collection
Sign Up labeled event signup_completed Yes product_events
Report Exported custom event report_exported Yes product_events
raw click No
pageview page_viewed for approved routes Maybe web_events

Heap labels can be applied retroactively. Record:

  • Heap event display name
  • downstream event table name
  • source type
  • last definition update
  • verification status where available
  • target event and schema version

Do not assume a current display label always represented the same product meaning across the full history.

Understand the Heap hierarchy#

Heap Connect models users, sessions, and events. User and session properties can appear beside event data. Preserve the distinction:

  • user identity belongs to the person
  • session identity groups activity
  • event properties describe the occurrence
  • current user attributes are not automatically event-time truth

If a user table contains today's plan or company, do not attach it to an event from two years ago and call it the historical plan. Use event-time properties or a separate versioned state model.

Map fields deliberately#

Heap field or concept GraphJSON
event table or metadata label bounded event
event_id source_event_id and deterministic event_id
user_id heap_user_id or mapped application user_id
session_id session_id
time request timestamp after verified conversion
source library source_platform
selected event properties approved event fields

Prefer application-owned user and account IDs when Heap contains them. Keep the Heap ID separately during reconciliation.

Heap Connect documents its event time as a timestamp without a time-zone type. Confirm how the export represents time in your destination and compare known events before converting to Unix seconds.

Normalize one row#

Example staging transformation:

function normalizeHeapEvent(row, eventName) {
  const occurredAt = new Date(row.time);

  if (Number.isNaN(occurredAt.getTime())) {
    throw new Error("Invalid Heap event time");
  }

  return {
    timestamp: Math.floor(occurredAt.getTime() / 1000),
    body: {
      event: eventName,
      event_id: `heap:${row.event_id}`,
      source_event_id: String(row.event_id),
      source: "heap",
      user_id: row.application_user_id || null,
      heap_user_id: String(row.user_id),
      session_id: String(row.session_id),
      schema_version: 1
    }
  };
}

Build one mapper per approved event family. Do not serialize the entire warehouse row into the event body.

Handle event labels and table names#

Heap Connect can create one table per selected labeled event and an all_events table for basic cross-event analysis. Keep a source metadata snapshot with each migration:

Heap table:
Heap UI label:
source type:
target event:
target schema:
mapping version:
export generated at:

If two old labels now map to one target event, preserve a source_definition field during the transition. If one Heap label changed meaning, split history by an explicit effective date instead of rewriting semantics silently.

Treat autocapture as a separate decision#

Autocaptured clicks, changes, and raw paths can be:

  • high volume
  • tied to unstable selectors
  • difficult to explain
  • privacy-sensitive
  • unnecessary for long-term product decisions

Select approved routes or interactions and normalize them into stable events. Do not import every selector simply because it is available.

For future collection, implement an explicit GraphJSON event contract rather than rebuilding Heap autocapture behavior.

Stage and import#

Use a migration manifest:

source export:
source tables or files:
source interval:
mapping version:
expected rows:
accepted rows:
quarantined rows:
target collections:
checksum or control totals:

Import chronological, bounded batches of at most 50 events through POST /api/bulk-log. Keep live traffic separate from backfill traffic and respect the documented workspace request limit.

Use High-volume ingestion and backfills for batching, retry, and quarantine behavior.

Reconcile meaning, not only rows#

Compare:

  • source rows selected
  • normalized rows
  • accepted and quarantined rows
  • unique source event IDs
  • min and max event time
  • counts by event and day
  • users and sessions by settled period
  • several governed funnel or retention results

Heap's UI and a raw export can use different retroactive definitions or processing behavior. Start with the exported rows as the migration control, then document expected differences from Heap reports.

Cut over future events#

Recommended sequence:

  1. implement explicit events in the application
  2. dual-send new activity for a bounded period
  3. compare identities, types, counts, and business metrics
  4. stop the historical import at an exact boundary
  5. stop Heap delivery after GraphJSON delivery is healthy
  6. preserve the source export and mapping manifest
  7. retire obsolete selectors and tracking calls deliberately

Avoid mixing imported and live events without a stable event_id, source, and cutover timestamp.

Migration checklist#

  • Export access and historical range are confirmed from a real sample.
  • Only decision-relevant labeled and custom events are selected.
  • Heap labels and definitions are snapshotted with the mapping.
  • User, session, and event properties keep their correct grains.
  • Current user state is not attached retroactively as event-time truth.
  • Event timestamps are verified against known occurrences.
  • Stable source identity supports deduplication.
  • Raw autocapture is minimized and normalized.
  • Batches, retries, and quarantine follow public GraphJSON limits.
  • Source, normalized, accepted, and quarantined counts reconcile.
  • Governed metrics are compared over settled windows.
  • Live cutover has an exact boundary and rollback plan.
Need a hand?

Tell us what you’re building and we’ll point you in the right direction.

Contact support