Skip to content

feat: add cross-origin reporting for telemetry in the dashboard #13612

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jun 20, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
feat: add cross-origin reporting for telemetry in the dashboard
  • Loading branch information
kylecarbs committed Jun 20, 2024
commit df9ecd70864a563f99886f67ad507d2fda00a5c1
1 change: 1 addition & 0 deletions site/jest.setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ global.scrollTo = jest.fn();

window.HTMLElement.prototype.scrollIntoView = jest.fn();
window.open = jest.fn();
navigator.sendBeacon = jest.fn();

// Polyfill the getRandomValues that is used on utils/random.ts
Object.defineProperty(global.self, "crypto", {
Expand Down
19 changes: 19 additions & 0 deletions site/src/pages/LoginPage/LoginPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useAuthContext } from "contexts/auth/AuthProvider";
import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata";
import { getApplicationName } from "utils/appearance";
import { retrieveRedirect } from "utils/redirect";
import { sendDeploymentEvent } from "utils/telemetry";
import { LoginPageView } from "./LoginPageView";

export const LoginPage: FC = () => {
Expand All @@ -19,6 +20,7 @@ export const LoginPage: FC = () => {
signIn,
isSigningIn,
signInError,
user,
} = useAuthContext();
const authMethodsQuery = useQuery(authMethods());
const redirectTo = retrieveRedirect(location.search);
Expand All @@ -29,6 +31,16 @@ export const LoginPage: FC = () => {
const buildInfoQuery = useQuery(buildInfo(metadata["build-info"]));

if (isSignedIn) {
// This uses `navigator.sendBeacon`, so window.href
// will not stop the request from being sent!
sendDeploymentEvent({
type: "deployment_login",
// This should work most of the time because of embedded
// metadata and the user being logged in.
deployment_id: buildInfoQuery.data?.deployment_id || "",
user_id: user?.id,
})

// If the redirect is going to a workspace application, and we
// are missing authentication, then we need to change the href location
// to trigger a HTTP request. This allows the BE to generate the auth
Expand Down Expand Up @@ -74,6 +86,13 @@ export const LoginPage: FC = () => {
isSigningIn={isSigningIn}
onSignIn={async ({ email, password }) => {
await signIn(email, password);
// This uses `navigator.sendBeacon`, so navigating away
// will not prevent it!
sendDeploymentEvent({
type: "deployment_login",
deployment_id: buildInfoQuery.data?.deployment_id || "",
user_id: user?.id,
})
navigate("/");
}}
/>
Expand Down
32 changes: 31 additions & 1 deletion site/src/pages/SetupPage/SetupPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import userEvent from "@testing-library/user-event";
import { HttpResponse, http } from "msw";
import { createMemoryRouter } from "react-router-dom";
import type { Response, User } from "api/typesGenerated";
import { MockUser } from "testHelpers/entities";
import { MockBuildInfo, MockUser } from "testHelpers/entities";
import {
renderWithRouter,
waitForLoaderToBeRemoved,
Expand Down Expand Up @@ -99,4 +99,34 @@ describe("Setup Page", () => {
await fillForm();
await waitFor(() => screen.findByText("Templates"));
});
it("calls sendBeacon with telemetry", async () => {
const sendBeacon = jest.fn();
Object.defineProperty(window.navigator, "sendBeacon", {
value: sendBeacon,
});
renderWithRouter(
createMemoryRouter(
[
{
path: "/setup",
element: <SetupPage />,
},
{
path: "/templates",
element: <h1>Templates</h1>,
},
],
{ initialEntries: ["/setup"] },
),
);
await waitForLoaderToBeRemoved();
await waitFor(() => {
expect(navigator.sendBeacon).toBeCalledWith("https://coder.com/api/track-deployment", new Blob([JSON.stringify({
type: "deployment_setup",
deployment_id: MockBuildInfo.deployment_id,
})], {
type: "application/json",
}))
})
})
});
25 changes: 22 additions & 3 deletions site/src/pages/SetupPage/SetupPage.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import type { FC } from "react";
import { useEffect, type FC } from "react";
import { Helmet } from "react-helmet-async";
import { useMutation } from "react-query";
import { useMutation, useQuery } from "react-query";
import { Navigate, useNavigate } from "react-router-dom";
import { buildInfo } from "api/queries/buildInfo";
import { createFirstUser } from "api/queries/users";
import { Loader } from "components/Loader/Loader";
import { useAuthContext } from "contexts/auth/AuthProvider";
import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata";
import { pageTitle } from "utils/page";
import { sendDeploymentEvent } from "utils/telemetry";
import { SetupPageView } from "./SetupPageView";

export const SetupPage: FC = () => {
export const SetupPage: FC<{
telemetryURL?: string;
}> = ({ telemetryURL }) => {
const {
isLoading,
signIn,
Expand All @@ -18,7 +23,21 @@ export const SetupPage: FC = () => {
} = useAuthContext();
const createFirstUserMutation = useMutation(createFirstUser());
const setupIsComplete = !isConfiguringTheFirstUser;
const { metadata } = useEmbeddedMetadata();
const buildInfoQuery = useQuery(buildInfo(metadata["build-info"]));
const navigate = useNavigate();
useEffect(() => {
if (!buildInfoQuery.data) {
return;
}
sendDeploymentEvent(
{
type: "deployment_setup",
deployment_id: buildInfoQuery.data.deployment_id,
},
telemetryURL,
);
}, [buildInfoQuery.data, telemetryURL]);

if (isLoading) {
return <Loader fullscreen />;
Expand Down
18 changes: 18 additions & 0 deletions site/src/utils/telemetry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// sendDeploymentEvent sends a CORs payload to coder.com
// to track a deployment event.
export const sendDeploymentEvent = (payload: {
type: "deployment_setup" | "deployment_login";
deployment_id: string;
user_id?: string;
}) => {
if (typeof navigator === "undefined" || !navigator.sendBeacon) {
// It's fine if we don't report this, it's not required!
return;
}
navigator.sendBeacon(
"https://coder.com/api/track-deployment",
new Blob([JSON.stringify(payload)], {
type: "application/json",
}),
);
};
Loading