diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go
index 31330cd175222..88d08e868690c 100644
--- a/coderd/apidoc/docs.go
+++ b/coderd/apidoc/docs.go
@@ -8351,6 +8351,10 @@ const docTemplate = `{
"description": "ExternalURL references the current Coder version.\nFor production builds, this will link directly to a release. For development builds, this will link to a commit.",
"type": "string"
},
+ "telemetry": {
+ "description": "Telemetry is a boolean that indicates whether telemetry is enabled.",
+ "type": "boolean"
+ },
"upgrade_message": {
"description": "UpgradeMessage is the message displayed to users when an outdated client\nis detected.",
"type": "string"
diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json
index 254eaa54c46dd..b6e527d0580d7 100644
--- a/coderd/apidoc/swagger.json
+++ b/coderd/apidoc/swagger.json
@@ -7430,6 +7430,10 @@
"description": "ExternalURL references the current Coder version.\nFor production builds, this will link directly to a release. For development builds, this will link to a commit.",
"type": "string"
},
+ "telemetry": {
+ "description": "Telemetry is a boolean that indicates whether telemetry is enabled.",
+ "type": "boolean"
+ },
"upgrade_message": {
"description": "UpgradeMessage is the message displayed to users when an outdated client\nis detected.",
"type": "string"
diff --git a/coderd/coderd.go b/coderd/coderd.go
index cc2de344a2cee..6de169cce71b7 100644
--- a/coderd/coderd.go
+++ b/coderd/coderd.go
@@ -447,6 +447,7 @@ func New(options *Options) *API {
WorkspaceProxy: false,
UpgradeMessage: api.DeploymentValues.CLIUpgradeMessage.String(),
DeploymentID: api.DeploymentID,
+ Telemetry: api.Telemetry.Enabled(),
}
api.SiteHandler = site.New(&site.Options{
BinFS: binFS,
diff --git a/coderd/telemetry/telemetry.go b/coderd/telemetry/telemetry.go
index 1b9489db3af8f..9d16ba7922098 100644
--- a/coderd/telemetry/telemetry.go
+++ b/coderd/telemetry/telemetry.go
@@ -93,6 +93,7 @@ type Reporter interface {
// database. For example, if a new user is added, a snapshot can
// contain just that user entry.
Report(snapshot *Snapshot)
+ Enabled() bool
Close()
}
@@ -109,6 +110,10 @@ type remoteReporter struct {
shutdownAt *time.Time
}
+func (*remoteReporter) Enabled() bool {
+ return true
+}
+
func (r *remoteReporter) Report(snapshot *Snapshot) {
go r.reportSync(snapshot)
}
@@ -948,4 +953,5 @@ type ExternalProvisioner struct {
type noopReporter struct{}
func (*noopReporter) Report(_ *Snapshot) {}
+func (*noopReporter) Enabled() bool { return false }
func (*noopReporter) Close() {}
diff --git a/codersdk/deployment.go b/codersdk/deployment.go
index ff35d67bacbb4..7b13d083a4435 100644
--- a/codersdk/deployment.go
+++ b/codersdk/deployment.go
@@ -2173,11 +2173,12 @@ type BuildInfoResponse struct {
ExternalURL string `json:"external_url"`
// Version returns the semantic version of the build.
Version string `json:"version"`
-
// DashboardURL is the URL to hit the deployment's dashboard.
// For external workspace proxies, this is the coderd they are connected
// to.
DashboardURL string `json:"dashboard_url"`
+ // Telemetry is a boolean that indicates whether telemetry is enabled.
+ Telemetry bool `json:"telemetry"`
WorkspaceProxy bool `json:"workspace_proxy"`
diff --git a/docs/api/general.md b/docs/api/general.md
index a92742ce0a707..620e3b238d7b3 100644
--- a/docs/api/general.md
+++ b/docs/api/general.md
@@ -57,6 +57,7 @@ curl -X GET http://coder-server:8080/api/v2/buildinfo \
"dashboard_url": "string",
"deployment_id": "string",
"external_url": "string",
+ "telemetry": true,
"upgrade_message": "string",
"version": "string",
"workspace_proxy": true
diff --git a/docs/api/schemas.md b/docs/api/schemas.md
index a5c333b3d0bd6..c2ee20e288d42 100644
--- a/docs/api/schemas.md
+++ b/docs/api/schemas.md
@@ -865,6 +865,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
"dashboard_url": "string",
"deployment_id": "string",
"external_url": "string",
+ "telemetry": true,
"upgrade_message": "string",
"version": "string",
"workspace_proxy": true
@@ -879,6 +880,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
| `dashboard_url` | string | false | | Dashboard URL is the URL to hit the deployment's dashboard. For external workspace proxies, this is the coderd they are connected to. |
| `deployment_id` | string | false | | Deployment ID is the unique identifier for this deployment. |
| `external_url` | string | false | | External URL references the current Coder version. For production builds, this will link directly to a release. For development builds, this will link to a commit. |
+| `telemetry` | boolean | false | | Telemetry is a boolean that indicates whether telemetry is enabled. |
| `upgrade_message` | string | false | | Upgrade message is the message displayed to users when an outdated client is detected. |
| `version` | string | false | | Version returns the semantic version of the build. |
| `workspace_proxy` | boolean | false | | |
diff --git a/enterprise/wsproxy/wsproxysdk/wsproxysdk_test.go b/enterprise/wsproxy/wsproxysdk/wsproxysdk_test.go
index 870d06b71da6d..c94b712cc9872 100644
--- a/enterprise/wsproxy/wsproxysdk/wsproxysdk_test.go
+++ b/enterprise/wsproxy/wsproxysdk/wsproxysdk_test.go
@@ -216,7 +216,7 @@ func TestDialCoordinator(t *testing.T) {
Node: &proto.Node{
Id: 55,
AsOf: timestamppb.New(time.Unix(1689653252, 0)),
- Key: peerNodeKey[:],
+ Key: peerNodeKey,
Disco: string(peerDiscoKey),
PreferredDerp: 0,
DerpLatency: map[string]float64{
diff --git a/site/jest.setup.ts b/site/jest.setup.ts
index 6282295870681..40bb92fa44965 100644
--- a/site/jest.setup.ts
+++ b/site/jest.setup.ts
@@ -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", {
diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts
index 0d9147c912e9e..052b2a6872b04 100644
--- a/site/src/api/typesGenerated.ts
+++ b/site/src/api/typesGenerated.ts
@@ -171,6 +171,7 @@ export interface BuildInfoResponse {
readonly external_url: string;
readonly version: string;
readonly dashboard_url: string;
+ readonly telemetry: boolean;
readonly workspace_proxy: boolean;
readonly agent_api_version: string;
readonly upgrade_message: string;
diff --git a/site/src/pages/LoginPage/LoginPage.tsx b/site/src/pages/LoginPage/LoginPage.tsx
index f05c0b40d981f..3fa2c5616be29 100644
--- a/site/src/pages/LoginPage/LoginPage.tsx
+++ b/site/src/pages/LoginPage/LoginPage.tsx
@@ -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 = () => {
@@ -19,6 +20,7 @@ export const LoginPage: FC = () => {
signIn,
isSigningIn,
signInError,
+ user,
} = useAuthContext();
const authMethodsQuery = useQuery(authMethods());
const redirectTo = retrieveRedirect(location.search);
@@ -29,6 +31,15 @@ export const LoginPage: FC = () => {
const buildInfoQuery = useQuery(buildInfo(metadata["build-info"]));
if (isSignedIn) {
+ if (buildInfoQuery.data) {
+ // This uses `navigator.sendBeacon`, so window.href
+ // will not stop the request from being sent!
+ sendDeploymentEvent(buildInfoQuery.data, {
+ type: "deployment_login",
+ 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
@@ -74,6 +85,15 @@ export const LoginPage: FC = () => {
isSigningIn={isSigningIn}
onSignIn={async ({ email, password }) => {
await signIn(email, password);
+ if (buildInfoQuery.data) {
+ // This uses `navigator.sendBeacon`, so navigating away
+ // will not prevent it!
+ sendDeploymentEvent(buildInfoQuery.data, {
+ type: "deployment_login",
+ user_id: user?.id,
+ });
+ }
+
navigate("/");
}}
/>
diff --git a/site/src/pages/SetupPage/SetupPage.test.tsx b/site/src/pages/SetupPage/SetupPage.test.tsx
index 2f558316d95cc..fb22dcf4f303a 100644
--- a/site/src/pages/SetupPage/SetupPage.test.tsx
+++ b/site/src/pages/SetupPage/SetupPage.test.tsx
@@ -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,
@@ -99,4 +99,42 @@ 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: