@@ -409,6 +409,118 @@ catalog.drop_table(("default", "<table-name>"), purge_requested=True)
409409PY
410410```
411411
412+ ## Import existing PostHog data
413+
414+ Hogflare includes a host-side importer for backfilling an existing PostHog project into the same Cloudflare Pipeline sink used by the Worker. It reads PostHog's private API with a personal API key, then writes normalized rows to the pipeline:
415+
416+ - persons as ` $identify ` rows
417+ - groups as ` $groupidentify ` rows
418+ - historical events from HogQL with original ` timestamp ` , ` created_at ` , and PostHog event ` uuid ` when available
419+
420+ The importer writes historical rows directly to the pipeline. It does not mutate Worker Durable Object state.
421+
422+ Required inputs:
423+
424+ ``` bash
425+ export POSTHOG_PROJECT_ID=" <project_id>"
426+ export POSTHOG_PERSONAL_API_KEY=" phx_..."
427+ export CLOUDFLARE_PIPELINE_ENDPOINT=" https://<stream-id>.ingest.cloudflare.com"
428+ export CLOUDFLARE_PIPELINE_AUTH_TOKEN=" <pipeline token>" # if your stream requires it
429+ ```
430+
431+ Optional inputs:
432+
433+ ``` bash
434+ export POSTHOG_HOST=" https://us.posthog.com" # or https://eu.posthog.com / self-hosted URL
435+ export POSTHOG_ENVIRONMENT_ID=" <environment_id>" # recommended for current PostHog persons/groups APIs
436+ export HOGFLARE_API_KEY=" phc_..."
437+ export POSTHOG_TEAM_ID=" 1"
438+ export POSTHOG_GROUP_TYPE_0=" company"
439+ export IMPORT_FROM=" 2025-01-01"
440+ export IMPORT_TO=" 2025-02-01"
441+ export IMPORT_BATCH_SIZE=" 500"
442+ export IMPORT_PERSONS_OFFSET=" 0" # resume guardrails
443+ export IMPORT_EVENTS_OFFSET=" 0"
444+ export IMPORT_EVENTS_AFTER_TIMESTAMP=" 2024-09-21T03:24:11Z"
445+ export IMPORT_EVENTS_AFTER_UUID=" 0192129b-c354-77b4-b496-9be7ec571fb4"
446+ export IMPORT_EVENT_UUIDS_FILE=" /tmp/missing-event-uuids.txt"
447+ export IMPORT_EVENT_WINDOW_DAYS=" 7"
448+ export IMPORT_EVENT_WINDOW_HOURS=" 6" # use days or hours, not both
449+ export IMPORT_MAX_PERSONS=" 1000" # optional guardrails for smoke tests
450+ export IMPORT_MAX_GROUPS=" 1000"
451+ export IMPORT_MAX_EVENTS=" 1000"
452+ export IMPORT_STATE_FILE=" .hogflare-import-state.jsonl"
453+ export IMPORT_TARGET_ACCOUNT_ID=" <cloudflare_account_id>"
454+ export IMPORT_TARGET_BUCKET=" <r2_bucket>"
455+ export IMPORT_TARGET_TABLE=" default.hogflare_events_v3"
456+ export WRANGLER_R2_SQL_AUTH_TOKEN=" <r2 sql token>"
457+ export IMPORT_CLOUDFLARE_API_TOKEN=" <token with Pipelines read>" # optional auto flush discovery
458+ export IMPORT_PIPELINE_FLUSH_SECS=" 300" # fallback if Pipelines read is unavailable
459+ ```
460+
461+ Production imports require R2 SQL target checks by default. The importer uses stable import
462+ keys, queries the target before each batch, and skips rows that are already present.
463+ Cloudflare Pipeline/R2 is append-only and does not enforce uniqueness by itself. Passing
464+ ` --no-target-check ` or ` IMPORT_TARGET_CHECKS=false ` opts out and should only be used for
465+ local tests.
466+
467+ Retry behavior is intentionally conservative. Import sends are not blindly retried after a
468+ transport or response error because the pipeline may have accepted the batch even if the
469+ client did not receive the response. The importer aligns its wait window to the Cloudflare
470+ Pipeline sink rolling policy when ` IMPORT_CLOUDFLARE_API_TOKEN ` can read Pipelines. Without
471+ that API access, it uses ` IMPORT_PIPELINE_FLUSH_SECS ` , defaulting conservatively to 300
472+ seconds. The wait is ` max(60s, 2 * flush + 30s) ` , unless ` IMPORT_TARGET_WAIT_SECS ` is set
473+ explicitly.
474+
475+ The local state file makes normal same-machine resumes cheap, but it is not a substitute for
476+ target checks if the state file is lost, multiple importers run concurrently, or a send has an
477+ unknown commit state.
478+
479+ Run a dry run first:
480+
481+ ``` bash
482+ cargo run --bin import_posthog -- --dry-run
483+ ```
484+
485+ Run the import:
486+
487+ ``` bash
488+ cargo run --bin import_posthog
489+ ```
490+
491+ You can also pass flags instead of env vars:
492+
493+ ``` bash
494+ cargo run --bin import_posthog -- \
495+ --posthog-host https://us.posthog.com \
496+ --project-id 12345 \
497+ --environment-id 67890 \
498+ --personal-api-key " $POSTHOG_PERSONAL_API_KEY " \
499+ --pipeline-endpoint " $CLOUDFLARE_PIPELINE_ENDPOINT " \
500+ --pipeline-auth-token " $CLOUDFLARE_PIPELINE_AUTH_TOKEN " \
501+ --hogflare-api-key phc_example \
502+ --from 2025-01-01 \
503+ --to 2025-02-01 \
504+ --persons-offset 0 \
505+ --events-offset 0 \
506+ --events-after-timestamp 2024-09-21T03:24:11Z \
507+ --events-after-uuid 0192129b-c354-77b4-b496-9be7ec571fb4 \
508+ --event-uuids-file /tmp/missing-event-uuids.txt \
509+ --event-window-hours 6 \
510+ --max-persons 1000 \
511+ --max-groups 1000 \
512+ --max-events 1000 \
513+ --import-state-file .hogflare-import-state.jsonl \
514+ --target-account-id " $CLOUDFLARE_ACCOUNT_ID " \
515+ --target-bucket hogflare \
516+ --target-table default.hogflare_events_v3 \
517+ --target-auth-token " $WRANGLER_R2_SQL_AUTH_TOKEN " \
518+ --cloudflare-api-token " $CLOUDFLARE_API_TOKEN "
519+ ```
520+
521+ Use ` --skip-persons ` , ` --skip-groups ` , or ` --skip-events ` to import only part of the project.
522+ Use ` --skip-person-output ` when resuming an event import after person rows were already written; it still loads people for event hydration.
523+
412524## PostHog compatibility
413525
414526### Ingestion endpoints
0 commit comments