# 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
///
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
///
interface ImportMetaEnv {
readonly VITE_APP_VERSION: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
```
Update `apps/external/src/vite-env.d.ts`:
```typescript
///
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.