client/web: add additional web client metrics logging (#10462)

Add additional web client metric logging. Namely, add logging events for
auth / deauth, enable / disable using exit node, enable / disable SSH,
enable / disable advertise routes, and click events on the device details
button.

Updates https://github.com/tailscale/tailscale/issues/10261

Signed-off-by: Mario Minardi <mario@tailscale.com>
pull/10503/head
Mario Minardi 6 months ago committed by GitHub
parent 97f84200ac
commit f5f21c213c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -119,3 +119,11 @@ type MetricType = "counter" | "gauge"
export type MetricName = export type MetricName =
| "web_client_advertise_exitnode_enable" | "web_client_advertise_exitnode_enable"
| "web_client_advertise_exitnode_disable" | "web_client_advertise_exitnode_disable"
| "web_client_use_exitnode_enable"
| "web_client_use_exitnode_disable"
| "web_client_ssh_enable"
| "web_client_ssh_disable"
| "web_client_node_connect"
| "web_client_node_disconnect"
| "web_client_advertise_routes_change"
| "web_client_device_details_click"

@ -3,7 +3,7 @@
import cx from "classnames" import cx from "classnames"
import React from "react" import React from "react"
import { apiFetch } from "src/api" import { apiFetch, incrementMetric } from "src/api"
import ACLTag from "src/components/acl-tag" import ACLTag from "src/components/acl-tag"
import * as Control from "src/components/control-components" import * as Control from "src/components/control-components"
import NiceIP from "src/components/nice-ip" import NiceIP from "src/components/nice-ip"
@ -40,11 +40,13 @@ export default function DeviceDetailsView({
{!readonly && ( {!readonly && (
<Button <Button
sizeVariant="small" sizeVariant="small"
onClick={() => onClick={() => {
// increment metrics before logout as we don't gracefully handle disconnect currently
incrementMetric("web_client_node_disconnect")
apiFetch("/local/v0/logout", "POST") apiFetch("/local/v0/logout", "POST")
.then(() => setLocation("/")) .then(() => setLocation("/"))
.catch((err) => alert("Logout failed: " + err.message)) .catch((err) => alert("Logout failed: " + err.message))
} }}
> >
Disconnect Disconnect
</Button> </Button>

@ -3,6 +3,7 @@
import cx from "classnames" import cx from "classnames"
import React, { useMemo } from "react" import React, { useMemo } from "react"
import { incrementMetric } from "src/api"
import { ReactComponent as ArrowRight } from "src/assets/icons/arrow-right.svg" import { ReactComponent as ArrowRight } from "src/assets/icons/arrow-right.svg"
import { ReactComponent as Machine } from "src/assets/icons/machine.svg" import { ReactComponent as Machine } from "src/assets/icons/machine.svg"
import AddressCard from "src/components/address-copy-card" import AddressCard from "src/components/address-copy-card"
@ -68,7 +69,11 @@ export default function HomeView({
disabled={readonly} disabled={readonly}
/> />
)} )}
<Link className="link font-medium" to="/details"> <Link
className="link font-medium"
to="/details"
onClick={() => incrementMetric("web_client_device_details_click")}
>
View device details &rarr; View device details &rarr;
</Link> </Link>
</div> </div>

@ -2,7 +2,7 @@
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
import React, { useCallback, useState } from "react" import React, { useCallback, useState } from "react"
import { apiFetch } from "src/api" import { apiFetch, incrementMetric } from "src/api"
import { ReactComponent as TailscaleIcon } from "src/assets/icons/tailscale-icon.svg" import { ReactComponent as TailscaleIcon } from "src/assets/icons/tailscale-icon.svg"
import { NodeData } from "src/hooks/node-data" import { NodeData } from "src/hooks/node-data"
import Button from "src/ui/button" import Button from "src/ui/button"
@ -25,7 +25,10 @@ export default function LoginView({
const login = useCallback( const login = useCallback(
(opt: TailscaleUpOptions) => { (opt: TailscaleUpOptions) => {
tailscaleUp(opt).then(refreshData) tailscaleUp(opt).then(() => {
incrementMetric("web_client_node_connect")
refreshData()
})
}, },
[refreshData] [refreshData]
) )

@ -148,9 +148,20 @@ export default function useNodeData() {
setIsPosting(false) setIsPosting(false)
mutate() // refresh data after PATCH finishes mutate() // refresh data after PATCH finishes
} }
const updateMetrics = () => {
// only update metrics if values have changed
if (data?.RunningSSHServer !== d.RunSSH) {
incrementMetric(
d.RunSSH ? "web_client_ssh_enable" : "web_client_ssh_disable"
)
}
}
return apiFetch("/local/v0/prefs", "PATCH", d) return apiFetch("/local/v0/prefs", "PATCH", d)
.then(onComplete) .then(() => {
updateMetrics()
onComplete()
})
.catch((err) => { .catch((err) => {
onComplete() onComplete()
alert("Failed to update prefs") alert("Failed to update prefs")
@ -176,6 +187,14 @@ export default function useNodeData() {
: "web_client_advertise_exitnode_disable" : "web_client_advertise_exitnode_disable"
) )
} }
// useExitNode is the ID of the exit node to use
if (data?.UsingExitNode?.ID !== d.UseExitNode) {
incrementMetric(
d.UseExitNode
? "web_client_use_exitnode_enable"
: "web_client_use_exitnode_disable"
)
}
} }
return apiFetch("/routes", "POST", d) return apiFetch("/routes", "POST", d)
@ -189,7 +208,7 @@ export default function useNodeData() {
throw err throw err
}) })
}, },
[mutate, data?.AdvertisingExitNode] [mutate, data?.AdvertisingExitNode, data?.UsingExitNode?.ID]
) )
const nodeUpdaters: NodeUpdaters = useMemo( const nodeUpdaters: NodeUpdaters = useMemo(
@ -209,7 +228,7 @@ export default function useNodeData() {
AdvertiseRoutes: routes, AdvertiseRoutes: routes,
AdvertiseExitNode: data?.AdvertisingExitNode, // unchanged AdvertiseExitNode: data?.AdvertisingExitNode, // unchanged
UseExitNode: data?.UsingExitNode?.ID, // unchanged UseExitNode: data?.UsingExitNode?.ID, // unchanged
}), }).then(() => incrementMetric("web_client_advertise_routes_change")),
}), }),
[ [
data?.AdvertisingExitNode, data?.AdvertisingExitNode,

Loading…
Cancel
Save