feat: Track collection events (#2256)

- Renames `inject_analytics` to `inject_extra` and updates docs
- Manually tracks page views to enable passing custom props
- Tracks copying collection share link and downloading a public
collection

---------

Co-authored-by: emma <hi@emma.cafe>
This commit is contained in:
sua yoo 2025-01-07 11:51:10 -08:00 committed by sua yoo
parent eb88e9f90c
commit b36ed9f730
No known key found for this signature in database
GPG Key ID: 5AD1B4C02D4F0567
13 changed files with 149 additions and 30 deletions

View File

@ -62,9 +62,9 @@ spec:
value: "{{ .Values.minio_local_bucket_name }}"
{{- end }}
{{- if .Values.inject_analytics }}
- name: INJECT_ANALYTICS
value: {{ .Values.inject_analytics }}
{{- if .Values.inject_extra }}
- name: INJECT_EXTRA
value: {{ .Values.inject_extra }}
{{- end }}
resources:

View File

@ -451,9 +451,9 @@ ingress:
ingress_class: nginx
# Optional: Analytics injection script
# This runs as a blocking script on the frontend, so usually you'll want to have it just add a single script tag to the page with the `defer` attribute.
# inject_analytics: // your analytics injection script here
# Optional: Front-end injected script
# This runs as a blocking script on the frontend, so usually you'll want to have it just add a single script tag to the page with the `defer` attribute. Useful for things like analytics and bug tracking.
# inject_extra: // your front-end injected script
# Signing Options

View File

@ -5,7 +5,7 @@ rm /etc/nginx/conf.d/default.conf
if [ -z "$LOCAL_MINIO_HOST" ]; then
echo "no local minio, clearing out minio route"
echo "" > /etc/nginx/includes/minio.conf
echo "" >/etc/nginx/includes/minio.conf
else
echo "local minio: replacing \$LOCAL_MINIO_HOST with \"$LOCAL_MINIO_HOST\", \$LOCAL_BUCKET with \"$LOCAL_BUCKET\""
sed -i "s/\$LOCAL_MINIO_HOST/$LOCAL_MINIO_HOST/g" /etc/nginx/includes/minio.conf
@ -13,15 +13,15 @@ else
fi
# Add analytics script, if provided
if [ -z "$INJECT_ANALYTICS" ]; then
if [ -z "$INJECT_EXTRA" ]; then
echo "analytics disabled, injecting blank script"
echo "" > /usr/share/nginx/html/extra.js
echo "" >/usr/share/nginx/html/extra.js
else
echo "analytics enabled, injecting script"
echo "$INJECT_ANALYTICS" > /usr/share/nginx/html/extra.js
echo "$INJECT_EXTRA" >/usr/share/nginx/html/extra.js
fi
mkdir -p /etc/nginx/resolvers/
echo resolver $(grep -oP '(?<=nameserver\s)[^\s]+' /etc/resolv.conf | awk '{ if ($1 ~ /:/) { printf "[" $1 "] "; } else { printf $1 " "; } }') valid=10s ipv6=off";" > /etc/nginx/resolvers/resolvers.conf
echo resolver $(grep -oP '(?<=nameserver\s)[^\s]+' /etc/resolv.conf | awk '{ if ($1 ~ /:/) { printf "[" $1 "] "; } else { printf $1 " "; } }') valid=10s ipv6=off";" >/etc/nginx/resolvers/resolvers.conf
cat /etc/nginx/resolvers/resolvers.conf

28
frontend/config/define.js Normal file
View File

@ -0,0 +1,28 @@
/**
* Global constants to make available to build
*
* @TODO Consolidate webpack and web-test-runner esbuild configs
*/
const path = require("path");
const isDevServer = process.env.WEBPACK_SERVE;
const dotEnvPath = path.resolve(
process.cwd(),
`.env${isDevServer ? `.local` : ""}`,
);
require("dotenv").config({
path: dotEnvPath,
});
const WEBSOCKET_HOST =
isDevServer && process.env.API_BASE_URL
? new URL(process.env.API_BASE_URL).host
: process.env.WEBSOCKET_HOST || "";
module.exports = {
"window.process.env.WEBSOCKET_HOST": JSON.stringify(WEBSOCKET_HOST),
"window.process.env.ANALYTICS_NAMESPACE": JSON.stringify(
process.env.ANALYTICS_NAMESPACE || "",
),
};

View File

@ -208,16 +208,22 @@ Browsertrix has the ability to cryptographically sign WACZ files with [Authsign]
You can enable sign-ups by setting `registration_enabled` to `"1"`. Once enabled, your users can register by visiting `/sign-up`.
## Analytics
## Inject Extra JavaScript
You can add a script to inject any sort of analytics into the frontend by setting `inject_analytics` to the script. If present, it will be injected as a blocking script tag into every page — so we recommend you create the script tags that handle your analytics from within this script.
You can add a script to inject analytics, bug reporting tools, etc. into the frontend by setting `inject_extra` to script contents of your choosing. If present, it will be injected as a blocking script tag that runs when the frontend web app is initialized.
For example, here's a script that adds Plausible Analytics tracking:
For example, enabling analytics and tracking might look like this:
```ts
const plausible = document.createElement("script");
plausible.src = "https://plausible.io/js/script.js";
plausible.defer = true;
plausible.dataset.domain = "app.browsertrix.com";
document.head.appendChild(plausible);
```yaml
inject_extra: >
const analytics = document.createElement("script");
analytics.src = "https://cdn.example.com/analytics.js";
analytics.defer = true;
document.head.appendChild(analytics);
window.analytics = window.analytics
|| function () { (window.analytics.q = window.analytics.q || []).push(arguments); };
```
Note that the script will only run when the web app loads, i.e. the first time the app is loaded in the browser and on hard refresh. The script will not run again upon clicking a link in the web app. This shouldn't be an issue with most analytics libraries, which should listen for changes to [window history](https://developer.mozilla.org/en-US/docs/Web/API/History). If you have a custom script that needs to re-run when the frontend URL changes, you'll need to add an event listener for the [`popstate` event](https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event).

View File

@ -3,3 +3,5 @@ DOCS_URL=https://docs.browsertrix.com/
E2E_USER_EMAIL=
E2E_USER_PASSWORD=
GLITCHTIP_DSN=
INJECT_EXTRA=
ANALYTICS_NAMESPACE=

View File

@ -20,11 +20,13 @@ import { SelectCollectionAccess } from "./select-collection-access";
import { BtrixElement } from "@/classes/BtrixElement";
import { ClipboardController } from "@/controllers/clipboard";
import { RouteNamespace } from "@/routes";
import { AnalyticsTrackEvent } from "@/trackEvents";
import {
CollectionAccess,
type Collection,
type PublicCollection,
} from "@/types/collection";
import { track } from "@/utils/analytics";
enum Tab {
Link = "link",
@ -113,6 +115,13 @@ export class ShareCollection extends BtrixElement {
?disabled=${!this.shareLink}
@click=${() => {
void this.clipboardController.copy(this.shareLink);
track(AnalyticsTrackEvent.CopyShareCollectionLink, {
org_slug: this.slug,
collection_id: this.collectionId,
collection_name: this.collection?.name,
logged_in: !!this.authState,
});
}}
>
<sl-icon
@ -188,6 +197,13 @@ export class ShareCollection extends BtrixElement {
href=${`/api/public/orgs/${this.slug}/collections/${this.collectionId}/download`}
download
?disabled=${!this.collection?.totalSize}
@click=${() => {
track(AnalyticsTrackEvent.DownloadPublicCollection, {
org_slug: this.slug,
collection_id: this.collectionId,
collection_name: this.collection?.name,
});
}}
>
<sl-icon name="cloud-download" slot="prefix"></sl-icon>
${msg("Download Collection")}

View File

@ -24,6 +24,7 @@ import "./styles.css";
import { OrgTab, RouteNamespace, ROUTES } from "./routes";
import type { UserInfo, UserOrg } from "./types/user";
import { pageView, type AnalyticsTrackProps } from "./utils/analytics";
import APIRouter, { type ViewState } from "./utils/APIRouter";
import AuthService, {
type AuthEventDetail,
@ -158,6 +159,10 @@ export class App extends BtrixElement {
);
}
firstUpdated() {
this.trackPageView();
}
willUpdate(changedProperties: Map<string, unknown>) {
if (changedProperties.has("settings")) {
AppStateService.updateSettings(this.settings || null);
@ -296,6 +301,22 @@ export class App extends BtrixElement {
} else {
window.history.pushState(this.viewState, "", urlStr);
}
this.trackPageView();
}
trackPageView() {
const { slug, collectionId } = this.viewState.params;
const pageViewProps: AnalyticsTrackProps = {
org_slug: slug || null,
logged_in: !!this.authState,
};
if (collectionId) {
pageViewProps.collection_id = collectionId;
}
pageView(pageViewProps);
}
render() {

View File

@ -0,0 +1,9 @@
/**
* All available analytics tracking events
*/
export enum AnalyticsTrackEvent {
PageView = "pageview",
CopyShareCollectionLink = "Copy share collection link",
DownloadPublicCollection = "Download public collection",
}

View File

@ -0,0 +1,40 @@
/**
* Custom tracking for analytics.
*
* Any third-party analytics script will need to have been made
* available through the `extra.js` injected by the server.
*/
import { AnalyticsTrackEvent } from "../trackEvents";
export type AnalyticsTrackProps = {
org_slug: string | null;
collection_id?: string | null;
collection_name?: string | null;
logged_in?: boolean;
};
export function track(
event: `${AnalyticsTrackEvent}`,
props?: AnalyticsTrackProps,
) {
// ANALYTICS_NAMESPACE is specified with webpack `DefinePlugin`
const analytics = window.process.env.ANALYTICS_NAMESPACE
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
(window as any)[window.process.env.ANALYTICS_NAMESPACE]
: null;
if (!analytics) {
return;
}
try {
analytics(event, { props });
} catch (err) {
console.debug(err);
}
}
export function pageView(props?: AnalyticsTrackProps) {
track(AnalyticsTrackEvent.PageView, props);
}

View File

@ -9,6 +9,8 @@ import { playwrightLauncher } from "@web/test-runner-playwright";
import glob from "glob";
import { typescriptPaths as typescriptPathsPlugin } from "rollup-plugin-typescript-paths";
import defineConfig from "./config/define.js";
const commonjs = fromRollup(commonjsPlugin);
const typescriptPaths = fromRollup(typescriptPathsPlugin);
@ -55,6 +57,7 @@ export default {
ts: true,
tsconfig: fileURLToPath(new URL("./tsconfig.json", import.meta.url)),
target: "esnext",
define: defineConfig,
}),
commonjs({
include: [

View File

@ -11,6 +11,7 @@ const HtmlWebpackPlugin = require("html-webpack-plugin");
const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");
const webpack = require("webpack");
const defineConfig = require("./config/define.js");
// @ts-ignore
const packageJSON = require("./package.json");
@ -24,11 +25,6 @@ require("dotenv").config({
path: dotEnvPath,
});
const WEBSOCKET_HOST =
isDevServer && process.env.API_BASE_URL
? new URL(process.env.API_BASE_URL).host
: process.env.WEBSOCKET_HOST || "";
const DOCS_URL = process.env.DOCS_URL
? new URL(process.env.DOCS_URL)
: isDevServer
@ -164,9 +160,7 @@ const main = {
),
}),
new webpack.DefinePlugin({
"window.process.env.WEBSOCKET_HOST": JSON.stringify(WEBSOCKET_HOST),
}),
new webpack.DefinePlugin(defineConfig),
new webpack.optimize.LimitChunkCountPlugin({
maxChunks: 12,

View File

@ -76,10 +76,10 @@ module.exports = [
res.status(404).send(`{"error": "placeholder_for_replay"}`);
});
// serve empty analytics script
// Serve analytics script, which is set in prod as an env variable by the Helm chart
server.app?.get("/extra.js", (req, res) => {
res.set("Content-Type", "application/javascript");
res.status(200).send("");
res.status(200).send(process.env.INJECT_EXTRA || "");
});
return middlewares;