Skip to content
Open
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,24 @@ Create a `.env.local` in your project root directory. It should contain the foll
```
NEXT_PUBLIC_GO_GETTA_PROD_URL=https://w6pkliozjh.execute-api.us-east-1.amazonaws.com/Stage
NEXT_PUBLIC_GOOGLE_MAPS_API_KEY=<insert a google maps API key here or contact us to get a development key>
NEXT_PUBLIC_DATADOG_APPLICATION_ID=<required for Browser RUM>
NEXT_PUBLIC_DATADOG_CLIENT_TOKEN=<required for Browser RUM>
NEXT_PUBLIC_DATADOG_SITE=datadoghq.com
NEXT_PUBLIC_DATADOG_SERVICE=yourpeer-frontend
NEXT_PUBLIC_DATADOG_ENV=production
NEXT_PUBLIC_DATADOG_VERSION=<set to release/version identifier>
NEXT_PUBLIC_DATADOG_APP_NAME=yourpeer.nyc
NEXT_PUBLIC_DATADOG_TRACING_ORIGINS=https://yourpeer.nyc,https://<api-gateway-domain>
NEXT_PUBLIC_DATADOG_ENABLED=false
NEXT_PUBLIC_DATADOG_REQUIRE_CONSENT=true
NEXT_PUBLIC_DATADOG_CONSENT_COOKIE_NAME=analytics_consent
NEXT_PUBLIC_DATADOG_SESSION_SAMPLE_RATE=100
NEXT_PUBLIC_DATADOG_SESSION_REPLAY_ENABLED=false
NEXT_PUBLIC_DATADOG_SESSION_REPLAY_SAMPLE_RATE=0
```

Datadog Browser RUM only initializes in the browser and is a no-op unless all of the following are true: `NEXT_PUBLIC_DATADOG_ENABLED=true`, required Datadog tokens are present, and consent is granted when `NEXT_PUBLIC_DATADOG_REQUIRE_CONSENT=true` (read from `NEXT_PUBLIC_DATADOG_CONSENT_COOKIE_NAME`).

Then run:

```
Expand Down
395 changes: 301 additions & 94 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"dependencies": {
"@aws-amplify/ui": "^6.7.0",
"@aws-amplify/ui-react": "^6.7.0",
"@datadog/browser-rum": "^6.30.1",
"@heroicons/react": "^2.1.4",
"@next/third-parties": "15.5.3",
"@radix-ui/react-alert-dialog": "^1.1.5",
Expand Down Expand Up @@ -65,6 +66,7 @@
"@types/node": "^20",
"@types/react": "19.1.13",
"@types/react-dom": "19.1.9",
"@types/react-test-renderer": "^19.1.0",
"@types/underscore": "^1.11.15",
"aws-cdk": "^2.170.0",
"aws-cdk-lib": "^2.170.0",
Expand All @@ -75,6 +77,7 @@
"eslint-config-next": "15.5.3",
"postcss": "^8",
"prettier": "^3.3.2",
"react-test-renderer": "^19.1.1",
"tailwind-scrollbar": "^3.1.0",
"tailwind-scrollbar-hide": "^1.1.7",
"tailwindcss": "^3.4.3",
Expand Down
2 changes: 2 additions & 0 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import Script from "next/script";
import { Toaster } from "sonner";
import QueryClientProvider from "@/app/QueryClientProvider";
import GTProdGuardScript from "@/components/gt-prod-guard-script";
import DatadogRumInit from "@/components/datadog-rum-init";
import { inter } from "./fonts";

export const viewport: Viewport = {
Expand Down Expand Up @@ -63,6 +64,7 @@ export default function RootLayout({
</LanguageTranslationProvider>
</CookiesProvider>
<Toaster />
<DatadogRumInit />
<GoogleAnalytics gaId={GOOGLE_ANALYTICS_MEASUREMENT_ID} />
<GoogleTagManager gtmId="GTM-ND2QBSQH" />
</body>
Expand Down
78 changes: 78 additions & 0 deletions src/components/datadog-rum-init-default-deps.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import test from "node:test";
import assert from "node:assert/strict";
import React from "react";
import { act, create } from "react-test-renderer";

const flushEffects = async () => {
await act(async () => {
await Promise.resolve();
});
};

test("DatadogRumInitComponent default deps do not duplicate startView on rerender", async () => {
process.env.NEXT_PUBLIC_DATADOG_ENABLED = "true";
process.env.NEXT_PUBLIC_DATADOG_APPLICATION_ID = "app-id";
process.env.NEXT_PUBLIC_DATADOG_CLIENT_TOKEN = "client-token";
process.env.NEXT_PUBLIC_DATADOG_REQUIRE_CONSENT = "false";

const mockWindow = {
clearInterval: () => undefined,
document: {
cookie: "",
},
location: { origin: "https://yourpeer.nyc" },
setInterval: (callback: TimerHandler) => {
if (typeof callback === "function") {
callback();
}

return 1;
},
} as unknown as Window;

(globalThis as { window?: Window }).window = mockWindow;

const datadogModule = await import("@datadog/browser-rum");
const datadogRum = datadogModule.datadogRum as {
getInitConfiguration: () => unknown;
init: (configuration: unknown) => void;
setTrackingConsent: (consent: "granted" | "not-granted") => void;
startView: (view: unknown) => void;
};

let startViewCount = 0;

datadogRum.getInitConfiguration = () => ({ initialized: true });
datadogRum.init = () => undefined;
datadogRum.setTrackingConsent = () => undefined;
datadogRum.startView = () => {
startViewCount += 1;
};

const moduleUrl = new URL(
`./datadog-rum-init.tsx?default-deps=${Date.now()}`,
import.meta.url,
);

const initModule = (await import(moduleUrl.href)) as {
DatadogRumInitComponent: React.ComponentType<{ pathname: string }>;
};

const renderer = create(
<initModule.DatadogRumInitComponent pathname="/jane-doe/private-area" />,
);

await flushEffects();
const initialCount = startViewCount;

await act(async () => {
renderer.update(
<initModule.DatadogRumInitComponent pathname="/jane-doe/private-area" />,
);
await Promise.resolve();
});

assert.equal(startViewCount, initialCount);

renderer.unmount();
});
89 changes: 89 additions & 0 deletions src/components/datadog-rum-init.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import test from "node:test";
import assert from "node:assert/strict";
import React from "react";
import { act, create } from "react-test-renderer";
import {
DatadogRumInitComponent,
type DatadogRumInitDeps,
} from "@/components/datadog-rum-init";

const mockWindow = {
clearInterval: () => undefined,
location: { origin: "https://yourpeer.nyc" },
setInterval: () => 1,
} as unknown as Window;

(globalThis as { window?: Window }).window = mockWindow;

const flushEffects = async () => {
await act(async () => {
await Promise.resolve();
});
};

test("DatadogRumInitComponent reacts to consent changes and preserves init/view ordering", async () => {
let consent = false;
let intervalCallback: (() => void) | undefined;
const callLog: string[] = [];

const deps: DatadogRumInitDeps = {
clearIntervalFn: (() => undefined) as typeof window.clearInterval,
getConsentStatus: () => consent,
initializeRumFn: ({ hasConsent }) => {
callLog.push(`init:${hasConsent ? "granted" : "not-granted"}`);
return hasConsent;
},
isRumAlreadyInitializedFn: () => false,
markRumAsInitializedFn: () => {
callLog.push("mark-initialized");
},
setIntervalFn: ((callback: TimerHandler) => {
intervalCallback = callback as () => void;
return 1;
}) as typeof window.setInterval,
startRumViewFn: ({ hasConsent, normalizedViewName }) => {
callLog.push(
`startView:${hasConsent ? "granted" : "not-granted"}:${normalizedViewName}`,
);
return hasConsent;
},
syncTrackingConsentFn: ({ hasConsent }) => {
callLog.push(`sync:${hasConsent ? "granted" : "not-granted"}`);
return true;
},
};

const renderer = create(
<DatadogRumInitComponent deps={deps} pathname="/jane-doe/private-area" />,
);

await flushEffects();

assert.ok(callLog.includes("sync:not-granted"));
assert.ok(callLog.includes("init:not-granted"));
assert.ok(
callLog.includes("startView:not-granted:route:/:slug/:slug"),
"view names should use slug-masked normalization",
);

consent = true;

await act(async () => {
intervalCallback?.();
await Promise.resolve();
});

const syncGrantedIndex = callLog.indexOf("sync:granted");
const initGrantedIndex = callLog.indexOf("init:granted");
const markInitializedIndex = callLog.indexOf("mark-initialized");
const startViewGrantedIndex = callLog.findIndex((entry) =>
entry.startsWith("startView:granted:"),
);

assert.ok(syncGrantedIndex >= 0);
assert.ok(initGrantedIndex > syncGrantedIndex);
assert.ok(markInitializedIndex > initGrantedIndex);
assert.ok(startViewGrantedIndex > markInitializedIndex);

renderer.unmount();
});
Loading