From 53ef3890ccb0f89568db6cc663d413273be75935 Mon Sep 17 00:00:00 2001 From: Kishore Kumar Date: Sun, 8 Mar 2026 15:14:17 +0530 Subject: [PATCH 1/4] fix(ci): make release fetch verification deterministic --- .github/workflows/release.yml | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b5592c4..8d6adcc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -49,7 +49,7 @@ jobs: cat release_notes.txt - name: Create GitHub Release - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v2 with: body_path: release_notes.txt name: "posthog-zig v${{ github.ref_name }}" @@ -65,10 +65,19 @@ jobs: - uses: mlugg/setup-zig@v2 with: version: 0.15.2 + - name: Create temp fetch workspace + run: | + FETCH_DIR="$(mktemp -d /tmp/posthog-fetch-XXXXXX)" + echo "FETCH_DIR=$FETCH_DIR" >> "$GITHUB_ENV" - name: Verify package is fetchable + working-directory: ${{ env.FETCH_DIR }} run: | TAG="${GITHUB_REF#refs/tags/}" - mkdir /tmp/fetch-test && cd /tmp/fetch-test + cat > build.zig << EOF + const std = @import("std"); + + pub fn build(_: *std.Build) void {} + EOF cat > build.zig.zon << EOF .{ .name = .@"fetch-test", @@ -81,5 +90,5 @@ jobs: .paths = .{ "build.zig.zon" }, } EOF - zig fetch --save https://github.com/usezombie/posthog-zig/archive/refs/tags/$TAG.tar.gz + zig fetch --save "https://github.com/usezombie/posthog-zig/archive/refs/tags/$TAG.tar.gz" echo "✓ package fetchable via zig fetch" From 2b8a00f5462b20b5df93e654ad4034a69677f2db Mon Sep 17 00:00:00 2001 From: Kishore Kumar Date: Sun, 8 Mar 2026 15:16:50 +0530 Subject: [PATCH 2/4] docs: update architecture notes --- docs/ARCHITECTURE.md | 94 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index ed020b7..26b6909 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -4,6 +4,100 @@ Design decisions, tradeoffs, and the target evolution of posthog-zig. --- +## End-to-end flow: from usezombie to PostHog + +``` +┌──────────────────────────────────────────────────────────────────────────────┐ +│ zombied (usezombie control plane) │ +│ │ +│ ┌─────────────────────┐ ┌──────────────────────┐ ┌──────────────────┐ │ +│ │ capture("run_start") │ │ captureException() │ │ identify() │ │ +│ │ capture("run_end") │ │ $exception_type │ │ group() │ │ +│ │ capture("deploy") │ │ $exception_message │ │ │ │ +│ │ + custom properties │ │ $exception_level │ │ $set traits │ │ +│ │ │ │ stack_trace (opt) │ │ $group_type/key │ │ +│ └──────────┬───────────┘ └──────────┬───────────┘ └────────┬─────────┘ │ +│ │ │ │ │ +│ └─────────────────┬───────┘───────────────────────┘ │ +│ ▼ │ +│ serialize to JSON (< 1μs) │ +│ + distinct_id, $lib, $lib_version, ISO 8601 timestamp │ +└──────────────────────────────┬─────────────────────────────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────────────────────────────────────┐ +│ posthog-zig SDK │ +│ │ +│ ┌─────────────────────────── Queue (batch.zig) ───────────────────────┐ │ +│ │ │ │ +│ │ enqueue() — mutex lock, arena dupe, append to event index, unlock │ │ +│ │ If count >= flush_at (default 20): signal condition variable │ │ +│ │ If count >= max_queue_size (default 1000): drop event (newest) │ │ +│ │ │ │ +│ │ Arena A (write side) Arena B (flush side) │ │ +│ │ [ev1_json][ev2_json][ev3]... [being POSTed] │ │ +│ │ │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌───────────────────── FlushThread (flush.zig) ──────────────────────┐ │ +│ │ │ │ +│ │ loop: │ │ +│ │ wait for signal OR flush_interval_ms timeout (default 10s) │ │ +│ │ drain() — swap write↔flush sides (O(1) index flip under mutex) │ │ +│ │ if no events: continue │ │ +│ │ POST /batch/ with all events from flush side │ │ +│ │ on 2xx: on_deliver(.delivered) → resetSide() (arena reclaim) │ │ +│ │ on 429/5xx: retry up to max_retries (default 3) │ │ +│ │ exponential backoff: min(1s × 2^attempt, 30s) + jitter │ │ +│ │ on 4xx: on_deliver(.failed) → drop batch, no retry │ │ +│ │ retries exhausted: on_deliver(.dropped) → drop batch │ │ +│ │ resetSide() — one arena reset, all memory reclaimed │ │ +│ │ │ │ +│ │ on shutdown: │ │ +│ │ final drain() + POST (best-effort, no retry on manual flush) │ │ +│ │ thread.join() │ │ +│ │ │ │ +│ └────────────────────────────┬───────────────────────────────────────┘ │ +│ │ │ +└───────────────────────────────┼────────────────────────────────────────────┘ + │ + ▼ HTTP POST +┌──────────────────────────────────────────────────────────────────────────────┐ +│ PostHog (https://us.i.posthog.com/batch/) │ +│ │ +│ { "api_key": "phc_...", "batch": [ {event}, {event}, ... ] } │ +│ │ +│ Events appear in: │ +│ • Events → custom events (capture) │ +│ • Error Tracking → $exception events (captureException) │ +│ • Persons → $identify events (identify) │ +│ • Groups → $groupidentify events (group) │ +└──────────────────────────────────────────────────────────────────────────────┘ +``` + +### What gets pushed + +| Method | PostHog event | Use case in zombied | +|---|---|---| +| `capture()` | Custom event name (e.g. `run_started`, `deploy_completed`) | Business analytics, usage tracking | +| `captureException()` | `$exception` with `$exception_type`, `$exception_message`, `$exception_level`, optional stack trace | Error tracking — caught errors, OOM, workspace failures | +| `identify()` | `$identify` with `$set` properties | Associate traits (email, plan) with a distinct_id | +| `group()` | `$groupidentify` with `$group_type`, `$group_key`, `$group_set` | Associate users with workspaces/orgs | + +### Key timing defaults + +| Parameter | Default | Purpose | +|---|---|---| +| `flush_interval_ms` | 10,000 (10s) | Max wait before background flush fires | +| `flush_at` | 20 events | Threshold to wake flush thread early | +| `max_queue_size` | 1,000 events | Per-side capacity; overflow drops newest | +| `max_retries` | 3 | Retry count for 429/5xx responses | +| `shutdown_flush_timeout_ms` | 5,000 (5s) | Reserved for v0.2 timed join | +| `feature_flag_ttl_ms` | 60,000 (60s) | Cache TTL for feature flag decisions | + +--- + ## Why this SDK exists posthog-zig is the server-side analytics layer for the usezombie stack. The first From f4079255020e36fee8070a3debfa1683a3834183 Mon Sep 17 00:00:00 2001 From: Kishore Kumar Date: Sun, 8 Mar 2026 15:19:04 +0530 Subject: [PATCH 3/4] feat(ci): auto-release on main merge --- .github/workflows/release.yml | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8d6adcc..b03c8ca 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,8 +2,8 @@ name: Release on: push: - tags: - - "v*" + branches: + - main permissions: contents: read @@ -22,15 +22,20 @@ jobs: # ── Verify VERSION matches git tag ──────────────────────────────────────── verify-version: runs-on: ubuntu-latest + outputs: + tag: ${{ steps.meta.outputs.tag }} + version: ${{ steps.meta.outputs.version }} steps: - uses: actions/checkout@v6 - - name: Check VERSION matches tag + - name: Resolve release version + id: meta run: | - TAG_VERSION="${GITHUB_REF#refs/tags/v}" - FILE_VERSION="$(cat VERSION | tr -d '[:space:]')" - echo "tag=$TAG_VERSION file=$FILE_VERSION" - [ "$TAG_VERSION" = "$FILE_VERSION" ] || { echo "✗ VERSION ($FILE_VERSION) does not match tag ($TAG_VERSION)"; exit 1; } - echo "✓ VERSION matches tag" + VERSION="$(tr -d '[:space:]' < VERSION)" + echo "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$' || { echo "✗ VERSION must be semver (x.y.z), got: $VERSION"; exit 1; } + TAG="v$VERSION" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "tag=$TAG" >> "$GITHUB_OUTPUT" + echo "✓ release version resolved: $VERSION ($TAG)" # ── GitHub Release with CHANGELOG excerpt ──────────────────────────────── create-release: @@ -43,7 +48,7 @@ jobs: - name: Extract changelog for this version run: | - VERSION="${GITHUB_REF#refs/tags/v}" + VERSION="${{ needs.verify-version.outputs.version }}" sed -n "/^## \[$VERSION\]/,/^## \[/p" CHANGELOG.md | sed '$d' > release_notes.txt echo "--- Release notes ---" cat release_notes.txt @@ -51,15 +56,17 @@ jobs: - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: + tag_name: ${{ needs.verify-version.outputs.tag }} + target_commitish: ${{ github.sha }} body_path: release_notes.txt - name: "posthog-zig v${{ github.ref_name }}" + name: "posthog-zig ${{ needs.verify-version.outputs.tag }}" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # ── Publish zig package (zig fetch verification) ────────────────────────── verify-fetchable: runs-on: ubuntu-latest - needs: create-release + needs: [create-release, verify-version] steps: - uses: actions/checkout@v6 - uses: mlugg/setup-zig@v2 @@ -72,7 +79,7 @@ jobs: - name: Verify package is fetchable working-directory: ${{ env.FETCH_DIR }} run: | - TAG="${GITHUB_REF#refs/tags/}" + TAG="${{ needs.verify-version.outputs.tag }}" cat > build.zig << EOF const std = @import("std"); From fc2efbb30242363ea3d092745f53832d64a4f7ab Mon Sep 17 00:00:00 2001 From: Kishore Kumar Date: Sun, 8 Mar 2026 15:29:19 +0530 Subject: [PATCH 4/4] docs: add end-to-end flow diagram (mermaid + ASCII) to ARCHITECTURE.md Amp-Thread-ID: https://ampcode.com/threads/T-019cccd3-db88-7436-ac7d-4b88f1b59b9f Co-authored-by: Amp --- docs/ARCHITECTURE.md | 62 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 26b6909..79dae6f 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -6,6 +6,66 @@ Design decisions, tradeoffs, and the target evolution of posthog-zig. ## End-to-end flow: from usezombie to PostHog +```mermaid +flowchart TD + subgraph zombied["zombied (usezombie control plane)"] + cap["capture()\nrun_started, deploy, run_end\n+ custom properties"] + exc["captureException()\n$exception_type, $exception_message\n$exception_level, stack_trace (opt)"] + ident["identify() — $set traits"] + grp["group() — $group_type / $group_key"] + end + + cap & exc & ident & grp --> ser["Serialize to JSON (< 1μs)\ndistinct_id, $lib, $lib_version, ISO 8601 timestamp"] + + subgraph sdk["posthog-zig SDK"] + subgraph queue["Queue — batch.zig"] + enq["enqueue()\nmutex lock → arena dupe → append → unlock"] + arenaA["Arena A (write side)\n[ev1][ev2][ev3]..."] + drop["count ≥ 1000? → drop newest"] + signal["count ≥ 20? → signal cond var"] + end + + subgraph flush["FlushThread — flush.zig"] + wait["timedWait() — OS parks thread (zero CPU)"] + wake["Wake on: signal (≥20) | timeout (10s) | shutdown"] + drain["drain() — swap write↔flush sides, O(1)"] + arenaB["Arena B (flush side) — owned exclusively"] + post["POST /batch/"] + ok["2xx → on_deliver(.delivered)"] + retryable["429/5xx → retry, backoff min(1s×2^n, 30s)+jitter, max 3"] + bad["4xx → on_deliver(.failed), drop"] + exhausted["Retries exhausted → on_deliver(.dropped)"] + reset["resetSide() — one arena reset, all memory reclaimed"] + end + end + + ser --> enq + enq --> arenaA + enq --> drop + enq --> signal + signal --> wake + wait --> wake + wake --> drain + drain --> arenaB + arenaB --> post + post --> ok + post --> retryable + post --> bad + retryable --> exhausted + ok & bad & exhausted --> reset + + post --> posthog + + subgraph posthog["PostHog (us.i.posthog.com/batch/)"] + events["Events — custom events"] + errors["Error Tracking — $exception"] + persons["Persons — $identify"] + groups["Groups — $groupidentify"] + end +``` + +
ASCII version (for terminals / non-Mermaid renderers) + ``` ┌──────────────────────────────────────────────────────────────────────────────┐ │ zombied (usezombie control plane) │ @@ -76,6 +136,8 @@ Design decisions, tradeoffs, and the target evolution of posthog-zig. └──────────────────────────────────────────────────────────────────────────────┘ ``` +
+ ### What gets pushed | Method | PostHog event | Use case in zombied |