# Datadog RUM Sourcemaps Via CI Implementation Plan

> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Generate private frontend sourcemaps for Datadog RUM/Error Tracking, upload them from GitHub Actions, and keep `.map` files out of deployed frontend assets.

**Architecture:** Add an explicit release-version contract to the main app’s RUM init, enable hidden sourcemaps in all Vite frontends, and create a frontend CI workflow that builds each app, uploads sourcemaps with matching Datadog metadata, and removes `.map` files before any deploy step consumes the build output.

**Tech Stack:** React 19, Vite 6, Vitest, GitHub Actions, Datadog CI CLI, Cloudflare/Wrangler deploy targets, pnpm workspaces.

**Spec:** `docs/superpowers/specs/2026-04-22-datadog-rum-sourcemaps-via-ci-design.md`

---

## File Structure

### New files

- `.github/workflows/frontend-sourcemaps.yml` — GitHub Actions workflow that builds the frontend apps on a self-hosted runner and uploads sourcemaps to Datadog.
- `apps/clients/src/vite-env.d.ts` — Vite env typings for client-specific build variables, including `VITE_APP_VERSION`.
- `apps/main/src/lib/rum.test.ts` — regression test for Datadog RUM init config.
- `apps/main/vite.config.test.ts` — production sourcemap config test for `apps/main`.
- `apps/clients/vite.config.test.ts` — production sourcemap config test for `apps/clients`.
- `apps/external/vite.config.test.ts` — production sourcemap config test for `apps/external`.

### Modified files

- `package.json` — add `@datadog/datadog-ci` to root dev dependencies.
- `apps/main/src/lib/rum.ts` — add `version: import.meta.env.VITE_APP_VERSION`.
- `apps/main/src/vite-env.d.ts` — add `VITE_APP_VERSION` typing.
- `apps/external/src/vite-env.d.ts` — add `VITE_APP_VERSION` typing.
- `apps/main/vite.config.ts` — enable hidden sourcemaps for non-test builds.
- `apps/clients/vite.config.ts` — enable hidden sourcemaps for non-test builds.
- `apps/external/vite.config.ts` — enable hidden sourcemaps for non-test builds.

---

## Chunk 1: Runtime Release Metadata

### Task 1: Wire Datadog RUM version in `apps/main`

**Files:**
- Create: `apps/main/src/lib/rum.test.ts`
- Modify: `apps/main/src/lib/rum.ts`
- Modify: `apps/main/src/vite-env.d.ts`

- [ ] **Step 1: Write the failing RUM init test**

Create `apps/main/src/lib/rum.test.ts`:

```typescript
import { beforeEach, describe, expect, it, vi } from "vitest";

const init = vi.fn();

vi.mock("@datadog/browser-rum", () => ({
  datadogRum: {
    init,
  },
}));

vi.mock("@datadog/browser-rum-react", () => ({
  reactPlugin: () => "react-plugin",
}));

describe("rum config", () => {
  beforeEach(() => {
    init.mockReset();
    vi.resetModules();
  });

  it("sets the Datadog release version from VITE_APP_VERSION", async () => {
    vi.stubEnv("VITE_APP_VERSION", "test-sha");
    vi.stubEnv("MODE", "production");

    await import("./rum");

    expect(init).toHaveBeenCalledWith(
      expect.objectContaining({
        service: "xtablo-ui",
        env: "production",
        version: "test-sha",
      })
    );
  });
});
```

- [ ] **Step 2: Run the test to verify it fails**

Run:

```bash
pnpm --filter @xtablo/main exec vitest run src/lib/rum.test.ts --mode dev
```

Expected: FAIL because `version` is not set in `datadogRum.init`.

- [ ] **Step 3: Add the runtime version**

Update `apps/main/src/lib/rum.ts`:

```typescript
  service: "xtablo-ui",
  env: import.meta.env.MODE,
  version: import.meta.env.VITE_APP_VERSION,
```

Update `apps/main/src/vite-env.d.ts`:

```typescript
/// <reference types="vite/client" />

interface ImportMetaEnv {
  readonly VITE_APP_VERSION: string;
}

interface ImportMeta {
  readonly env: ImportMetaEnv;
}
```

- [ ] **Step 4: Run the test to verify it passes**

Run:

```bash
pnpm --filter @xtablo/main exec vitest run src/lib/rum.test.ts --mode dev
pnpm --filter @xtablo/main typecheck
```

Expected: PASS.

- [ ] **Step 5: Commit**

```bash
git add apps/main/src/lib/rum.ts apps/main/src/lib/rum.test.ts apps/main/src/vite-env.d.ts
git commit -m "feat: add Datadog release version to main rum config"
```

---

## Chunk 2: Hidden Sourcemaps In All Vite Frontends

### Task 2: Add production sourcemap config tests

**Files:**
- Create: `apps/main/vite.config.test.ts`
- Create: `apps/clients/vite.config.test.ts`
- Create: `apps/external/vite.config.test.ts`

- [ ] **Step 1: Write the failing config test for `apps/main`**

Create `apps/main/vite.config.test.ts`:

```typescript
import { describe, expect, it } from "vitest";
import configFactory from "./vite.config";

describe("main vite config", () => {
  it("uses hidden sourcemaps for production builds", () => {
    const config = configFactory({ mode: "production" });
    expect(config.build?.sourcemap).toBe("hidden");
  });
});
```

- [ ] **Step 2: Write the failing config test for `apps/clients`**

Create `apps/clients/vite.config.test.ts`:

```typescript
import { describe, expect, it } from "vitest";
import configFactory from "./vite.config";

describe("clients vite config", () => {
  it("uses hidden sourcemaps for production builds", () => {
    const config = configFactory({ mode: "production" });
    expect(config.build?.sourcemap).toBe("hidden");
  });
});
```

- [ ] **Step 3: Write the failing config test for `apps/external`**

Create `apps/external/vite.config.test.ts`:

```typescript
import { describe, expect, it } from "vitest";
import configFactory from "./vite.config";

describe("external vite config", () => {
  it("uses hidden sourcemaps for production builds", () => {
    const config = configFactory({ mode: "production" });
    expect(config.build?.sourcemap).toBe("hidden");
  });
});
```

- [ ] **Step 4: Run the tests to verify they fail**

Run:

```bash
pnpm --filter @xtablo/main exec vitest run vite.config.test.ts --mode dev
pnpm --filter @xtablo/clients exec vitest run vite.config.test.ts --mode test
pnpm --filter @xtablo/external exec vitest run vite.config.test.ts --mode test
```

Expected: FAIL because `build.sourcemap` is not configured.

- [ ] **Step 5: Implement hidden sourcemaps in all three Vite configs**

Update:
- `apps/main/vite.config.ts`
- `apps/clients/vite.config.ts`
- `apps/external/vite.config.ts`

Add the same build section in each returned config:

```typescript
    build: {
      sourcemap: mode === "test" ? false : "hidden",
    },
```

Keep it alongside the existing `plugins`, `server`, `define`, and `test` sections.

- [ ] **Step 6: Add env typing for the remaining apps**

Create `apps/clients/src/vite-env.d.ts`:

```typescript
/// <reference types="vite/client" />

interface ImportMetaEnv {
  readonly VITE_APP_VERSION: string;
}

interface ImportMeta {
  readonly env: ImportMetaEnv;
}
```

Update `apps/external/src/vite-env.d.ts`:

```typescript
/// <reference types="vite/client" />

interface ImportMetaEnv {
  readonly VITE_APP_VERSION: string;
}

interface ImportMeta {
  readonly env: ImportMetaEnv;
}
```

- [ ] **Step 7: Run the tests and typechecks**

Run:

```bash
pnpm --filter @xtablo/main exec vitest run vite.config.test.ts --mode dev
pnpm --filter @xtablo/clients exec vitest run vite.config.test.ts --mode test
pnpm --filter @xtablo/external exec vitest run vite.config.test.ts --mode test
pnpm --filter @xtablo/main typecheck
pnpm --filter @xtablo/clients typecheck
pnpm --filter @xtablo/external typecheck
```

Expected: PASS.

- [ ] **Step 8: Commit**

```bash
git add apps/main/vite.config.ts apps/main/vite.config.test.ts apps/clients/vite.config.ts apps/clients/vite.config.test.ts apps/clients/src/vite-env.d.ts apps/external/vite.config.ts apps/external/vite.config.test.ts apps/external/src/vite-env.d.ts
git commit -m "build: enable hidden sourcemaps for frontend apps"
```

---

## Chunk 3: Datadog CI Upload Workflow

### Task 3: Add pinned Datadog CLI and frontend sourcemap workflow

**Files:**
- Create: `.github/workflows/frontend-sourcemaps.yml`
- Modify: `package.json`

- [ ] **Step 1: Add the Datadog CLI dependency**

Update root `package.json`:

```json
  "devDependencies": {
    "@biomejs/biome": "2.2.5",
    "@datadog/datadog-ci": "^3.21.0",
    "turbo": "^2.5.8",
    "typescript": "^5.7.0"
  }
```

- [ ] **Step 2: Install dependencies and verify lockfile changes**

Run:

```bash
pnpm install
```

Expected: `pnpm-lock.yaml` updates with `@datadog/datadog-ci`.

- [ ] **Step 3: Create the workflow**

Create `.github/workflows/frontend-sourcemaps.yml`:

```yaml
name: Frontend Sourcemaps

on:
  workflow_dispatch:
  push:
    branches:
      - main
      - develop

jobs:
  upload-sourcemaps:
    runs-on:
      - self-hosted
      - linux
      - x64
    env:
      DATADOG_API_KEY: ${{ secrets.DATADOG_API_KEY }}
      DATADOG_SITE: ${{ secrets.DATADOG_SITE }}
      RELEASE_VERSION: ${{ github.sha }}
    steps:
      - uses: actions/checkout@v6

      - uses: pnpm/action-setup@v5
        with:
          version: 10.19.0

      - uses: actions/setup-node@v6
        with:
          node-version: 22
          cache: pnpm

      - name: Install dependencies
        run: pnpm install --frozen-lockfile --child-concurrency=2

      - name: Build main
        run: pnpm --filter @xtablo/main build:prod
        env:
          VITE_APP_VERSION: ${{ env.RELEASE_VERSION }}

      - name: Upload main sourcemaps
        run: pnpm exec datadog-ci sourcemaps upload apps/main/dist --service xtablo-ui --release-version "$RELEASE_VERSION" --minified-path-prefix https://app.xtablo.com/assets

      - name: Remove main sourcemaps
        run: find apps/main/dist -name '*.map' -delete

      - name: Build clients
        run: pnpm --filter @xtablo/clients build:prod
        env:
          VITE_APP_VERSION: ${{ env.RELEASE_VERSION }}

      - name: Upload clients sourcemaps
        run: pnpm exec datadog-ci sourcemaps upload apps/clients/dist --service xtablo-clients --release-version "$RELEASE_VERSION" --minified-path-prefix https://clients.xtablo.com/assets

      - name: Remove clients sourcemaps
        run: find apps/clients/dist -name '*.map' -delete

      - name: Build external
        run: pnpm --filter @xtablo/external build
        env:
          VITE_APP_VERSION: ${{ env.RELEASE_VERSION }}

      - name: Upload external sourcemaps
        run: pnpm exec datadog-ci sourcemaps upload apps/external/dist --service xtablo-external --release-version "$RELEASE_VERSION" --minified-path-prefix https://embed.xtablo.com/assets

      - name: Remove external sourcemaps
        run: find apps/external/dist -name '*.map' -delete
```

- [ ] **Step 4: Validate the workflow file structure**

Run:

```bash
rg -n "datadog-ci sourcemaps upload|RELEASE_VERSION|find .*\\.map" .github/workflows/frontend-sourcemaps.yml
```

Expected: one upload and one cleanup step for each frontend app.

- [ ] **Step 5: Commit**

```bash
git add package.json pnpm-lock.yaml .github/workflows/frontend-sourcemaps.yml
git commit -m "ci: upload frontend sourcemaps to Datadog"
```

---

## Chunk 4: End-To-End Verification

### Task 4: Prove builds emit hidden sourcemaps and cleanup works

**Files:**
- No new source files

- [ ] **Step 1: Build each frontend locally with a release version**

Run:

```bash
VITE_APP_VERSION=test-sha pnpm --filter @xtablo/main build:prod
VITE_APP_VERSION=test-sha pnpm --filter @xtablo/clients build:prod
VITE_APP_VERSION=test-sha pnpm --filter @xtablo/external build
```

Expected: each `dist/` contains built assets and `.map` files, but the generated JS bundles do not include public `sourceMappingURL` comments.

- [ ] **Step 2: Verify `.map` files exist before cleanup**

Run:

```bash
find apps/main/dist apps/clients/dist apps/external/dist -name '*.map' | sort
```

Expected: at least one sourcemap per app.

- [ ] **Step 3: Simulate CI cleanup locally**

Run:

```bash
find apps/main/dist apps/clients/dist apps/external/dist -name '*.map' -delete
find apps/main/dist apps/clients/dist apps/external/dist -name '*.map' | sort
```

Expected: no output from the second command.

- [ ] **Step 4: Run the targeted regression suite**

Run:

```bash
pnpm --filter @xtablo/main exec vitest run src/lib/rum.test.ts vite.config.test.ts --mode dev
pnpm --filter @xtablo/clients exec vitest run vite.config.test.ts --mode test
pnpm --filter @xtablo/external exec vitest run vite.config.test.ts --mode test
pnpm --filter @xtablo/main typecheck
pnpm --filter @xtablo/clients typecheck
pnpm --filter @xtablo/external typecheck
```

Expected: PASS.

- [ ] **Step 5: Commit**

```bash
git status --short
```

Expected: clean worktree before opening the implementation PR.
