Skip to content

Commit df9ecd7

Browse files
committed
feat: add cross-origin reporting for telemetry in the dashboard
1 parent a1db6d8 commit df9ecd7

File tree

5 files changed

+91
-4
lines changed

5 files changed

+91
-4
lines changed

site/jest.setup.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ global.scrollTo = jest.fn();
4141

4242
window.HTMLElement.prototype.scrollIntoView = jest.fn();
4343
window.open = jest.fn();
44+
navigator.sendBeacon = jest.fn();
4445

4546
// Polyfill the getRandomValues that is used on utils/random.ts
4647
Object.defineProperty(global.self, "crypto", {

site/src/pages/LoginPage/LoginPage.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { useAuthContext } from "contexts/auth/AuthProvider";
88
import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata";
99
import { getApplicationName } from "utils/appearance";
1010
import { retrieveRedirect } from "utils/redirect";
11+
import { sendDeploymentEvent } from "utils/telemetry";
1112
import { LoginPageView } from "./LoginPageView";
1213

1314
export const LoginPage: FC = () => {
@@ -19,6 +20,7 @@ export const LoginPage: FC = () => {
1920
signIn,
2021
isSigningIn,
2122
signInError,
23+
user,
2224
} = useAuthContext();
2325
const authMethodsQuery = useQuery(authMethods());
2426
const redirectTo = retrieveRedirect(location.search);
@@ -29,6 +31,16 @@ export const LoginPage: FC = () => {
2931
const buildInfoQuery = useQuery(buildInfo(metadata["build-info"]));
3032

3133
if (isSignedIn) {
34+
// This uses `navigator.sendBeacon`, so window.href
35+
// will not stop the request from being sent!
36+
sendDeploymentEvent({
37+
type: "deployment_login",
38+
// This should work most of the time because of embedded
39+
// metadata and the user being logged in.
40+
deployment_id: buildInfoQuery.data?.deployment_id || "",
41+
user_id: user?.id,
42+
})
43+
3244
// If the redirect is going to a workspace application, and we
3345
// are missing authentication, then we need to change the href location
3446
// to trigger a HTTP request. This allows the BE to generate the auth
@@ -74,6 +86,13 @@ export const LoginPage: FC = () => {
7486
isSigningIn={isSigningIn}
7587
onSignIn={async ({ email, password }) => {
7688
await signIn(email, password);
89+
// This uses `navigator.sendBeacon`, so navigating away
90+
// will not prevent it!
91+
sendDeploymentEvent({
92+
type: "deployment_login",
93+
deployment_id: buildInfoQuery.data?.deployment_id || "",
94+
user_id: user?.id,
95+
})
7796
navigate("/");
7897
}}
7998
/>

site/src/pages/SetupPage/SetupPage.test.tsx

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import userEvent from "@testing-library/user-event";
33
import { HttpResponse, http } from "msw";
44
import { createMemoryRouter } from "react-router-dom";
55
import type { Response, User } from "api/typesGenerated";
6-
import { MockUser } from "testHelpers/entities";
6+
import { MockBuildInfo, MockUser } from "testHelpers/entities";
77
import {
88
renderWithRouter,
99
waitForLoaderToBeRemoved,
@@ -99,4 +99,34 @@ describe("Setup Page", () => {
9999
await fillForm();
100100
await waitFor(() => screen.findByText("Templates"));
101101
});
102+
it("calls sendBeacon with telemetry", async () => {
103+
const sendBeacon = jest.fn();
104+
Object.defineProperty(window.navigator, "sendBeacon", {
105+
value: sendBeacon,
106+
});
107+
renderWithRouter(
108+
createMemoryRouter(
109+
[
110+
{
111+
path: "/setup",
112+
element: <SetupPage />,
113+
},
114+
{
115+
path: "/templates",
116+
element: <h1>Templates</h1>,
117+
},
118+
],
119+
{ initialEntries: ["/setup"] },
120+
),
121+
);
122+
await waitForLoaderToBeRemoved();
123+
await waitFor(() => {
124+
expect(navigator.sendBeacon).toBeCalledWith("https://coder.com/api/track-deployment", new Blob([JSON.stringify({
125+
type: "deployment_setup",
126+
deployment_id: MockBuildInfo.deployment_id,
127+
})], {
128+
type: "application/json",
129+
}))
130+
})
131+
})
102132
});

site/src/pages/SetupPage/SetupPage.tsx

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
1-
import type { FC } from "react";
1+
import { useEffect, type FC } from "react";
22
import { Helmet } from "react-helmet-async";
3-
import { useMutation } from "react-query";
3+
import { useMutation, useQuery } from "react-query";
44
import { Navigate, useNavigate } from "react-router-dom";
5+
import { buildInfo } from "api/queries/buildInfo";
56
import { createFirstUser } from "api/queries/users";
67
import { Loader } from "components/Loader/Loader";
78
import { useAuthContext } from "contexts/auth/AuthProvider";
9+
import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata";
810
import { pageTitle } from "utils/page";
11+
import { sendDeploymentEvent } from "utils/telemetry";
912
import { SetupPageView } from "./SetupPageView";
1013

11-
export const SetupPage: FC = () => {
14+
export const SetupPage: FC<{
15+
telemetryURL?: string;
16+
}> = ({ telemetryURL }) => {
1217
const {
1318
isLoading,
1419
signIn,
@@ -18,7 +23,21 @@ export const SetupPage: FC = () => {
1823
} = useAuthContext();
1924
const createFirstUserMutation = useMutation(createFirstUser());
2025
const setupIsComplete = !isConfiguringTheFirstUser;
26+
const { metadata } = useEmbeddedMetadata();
27+
const buildInfoQuery = useQuery(buildInfo(metadata["build-info"]));
2128
const navigate = useNavigate();
29+
useEffect(() => {
30+
if (!buildInfoQuery.data) {
31+
return;
32+
}
33+
sendDeploymentEvent(
34+
{
35+
type: "deployment_setup",
36+
deployment_id: buildInfoQuery.data.deployment_id,
37+
},
38+
telemetryURL,
39+
);
40+
}, [buildInfoQuery.data, telemetryURL]);
2241

2342
if (isLoading) {
2443
return <Loader fullscreen />;

site/src/utils/telemetry.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// sendDeploymentEvent sends a CORs payload to coder.com
2+
// to track a deployment event.
3+
export const sendDeploymentEvent = (payload: {
4+
type: "deployment_setup" | "deployment_login";
5+
deployment_id: string;
6+
user_id?: string;
7+
}) => {
8+
if (typeof navigator === "undefined" || !navigator.sendBeacon) {
9+
// It's fine if we don't report this, it's not required!
10+
return;
11+
}
12+
navigator.sendBeacon(
13+
"https://coder.com/api/track-deployment",
14+
new Blob([JSON.stringify(payload)], {
15+
type: "application/json",
16+
}),
17+
);
18+
};

0 commit comments

Comments
 (0)