12 KiB
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, includingVITE_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 forapps/main.apps/clients/vite.config.test.ts— production sourcemap config test forapps/clients.apps/external/vite.config.test.ts— production sourcemap config test forapps/external.
Modified files
package.json— add@datadog/datadog-cito root dev dependencies.apps/main/src/lib/rum.ts— addversion: import.meta.env.VITE_APP_VERSION.apps/main/src/vite-env.d.ts— addVITE_APP_VERSIONtyping.apps/external/src/vite-env.d.ts— addVITE_APP_VERSIONtyping.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:
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:
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:
service: "xtablo-ui",
env: import.meta.env.MODE,
version: import.meta.env.VITE_APP_VERSION,
Update apps/main/src/vite-env.d.ts:
/// <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:
pnpm --filter @xtablo/main exec vitest run src/lib/rum.test.ts --mode dev
pnpm --filter @xtablo/main typecheck
Expected: PASS.
- Step 5: Commit
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:
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:
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:
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:
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.tsapps/clients/vite.config.tsapps/external/vite.config.ts
Add the same build section in each returned config:
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:
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_APP_VERSION: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
Update apps/external/src/vite-env.d.ts:
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_APP_VERSION: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
- Step 7: Run the tests and typechecks
Run:
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
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:
"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:
pnpm install
Expected: pnpm-lock.yaml updates with @datadog/datadog-ci.
- Step 3: Create the workflow
Create .github/workflows/frontend-sourcemaps.yml:
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:
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
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:
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
.mapfiles exist before cleanup
Run:
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:
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:
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
git status --short
Expected: clean worktree before opening the implementation PR.