client/web: add endpoint for logging device detail click metric (#10505)

Add an endpoint for logging the device detail click metric to allow for
this metric to be logged without having a valid session which is the
case when in readonly mode.

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

Signed-off-by: Mario Minardi <mario@tailscale.com>
This commit is contained in:
Mario Minardi 2023-12-11 10:50:06 -07:00 committed by GitHub
parent d8493d4bd5
commit 109929d110
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 25 additions and 3 deletions

View File

@ -369,4 +369,3 @@ export type MetricName =
| "web_client_node_connect"
| "web_client_node_disconnect"
| "web_client_advertise_routes_change"
| "web_client_device_details_click"

View File

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

View File

@ -336,6 +336,9 @@ func (s *Server) authorizeRequest(w http.ResponseWriter, r *http.Request) (ok bo
case r.URL.Path == "/api/data" && r.Method == httpm.GET:
// Readonly endpoint allowed without valid browser session.
return true
case r.URL.Path == "/api/device-details-click" && r.Method == httpm.POST:
// Special case metric endpoint that is allowed without a browser session.
return true
case strings.HasPrefix(r.URL.Path, "/api/"):
// All other /api/ endpoints require a valid browser session.
if err != nil || !session.isAuthorized(s.timeNow()) {
@ -371,6 +374,8 @@ func (s *Server) serveLoginAPI(w http.ResponseWriter, r *http.Request) {
s.serveGetNodeData(w, r)
case r.URL.Path == "/api/up" && r.Method == httpm.POST:
s.serveTailscaleUp(w, r)
case r.URL.Path == "/api/device-details-click" && r.Method == httpm.POST:
s.serveDeviceDetailsClick(w, r)
default:
http.Error(w, "invalid endpoint or method", http.StatusNotFound)
}
@ -549,6 +554,9 @@ func (s *Server) serveAPI(w http.ResponseWriter, r *http.Request) {
case path == "/routes" && r.Method == httpm.POST:
s.servePostRoutes(w, r)
return
case path == "/device-details-click" && r.Method == httpm.POST:
s.serveDeviceDetailsClick(w, r)
return
case strings.HasPrefix(path, "/local/"):
s.proxyRequestToLocalAPI(w, r)
return
@ -970,6 +978,21 @@ func (s *Server) serveTailscaleUp(w http.ResponseWriter, r *http.Request) {
}
}
// serveDeviceDetailsClick increments the web_client_device_details_click metric
// by one.
//
// Metric logging from the frontend typically is proxied to the localapi. This event
// has been special cased as access to the localapi is gated upon having a valid
// session which is not always the case when we want to be logging this metric (e.g.,
// when in readonly mode).
//
// Other metrics should not be logged in this way without a good reason.
func (s *Server) serveDeviceDetailsClick(w http.ResponseWriter, r *http.Request) {
s.lc.IncrementCounter(r.Context(), "web_client_device_details_click", 1)
io.WriteString(w, "{}")
}
// proxyRequestToLocalAPI proxies the web API request to the localapi.
//
// The web API request path is expected to exactly match a localapi path,