Skip to content

Commit

Permalink
feat: Track collection events (#2256)
Browse files Browse the repository at this point in the history
- 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 <[email protected]>
  • Loading branch information
SuaYoo and emma-sg committed Jan 7, 2025
1 parent a716265 commit ddf6fd4
Show file tree
Hide file tree
Showing 13 changed files with 149 additions and 30 deletions.
6 changes: 3 additions & 3 deletions chart/templates/frontend.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
6 changes: 3 additions & 3 deletions chart/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 5 additions & 5 deletions frontend/00-browsertrix-nginx-init.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,23 @@ 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
sed -i "s/\$LOCAL_BUCKET/$LOCAL_BUCKET/g" /etc/nginx/includes/minio.conf
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 changes: 28 additions & 0 deletions frontend/config/define.js
Original file line number Diff line number Diff line change
@@ -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 || "",
),
};
24 changes: 15 additions & 9 deletions frontend/docs/docs/deploy/customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
2 changes: 2 additions & 0 deletions frontend/sample.env.local
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ DOCS_URL=https://docs.browsertrix.com/
E2E_USER_EMAIL=
E2E_USER_PASSWORD=
GLITCHTIP_DSN=
INJECT_EXTRA=
ANALYTICS_NAMESPACE=
16 changes: 16 additions & 0 deletions frontend/src/features/collections/share-collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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")}
Expand Down
21 changes: 21 additions & 0 deletions frontend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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() {
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/trackEvents.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* All available analytics tracking events
*/

export enum AnalyticsTrackEvent {
PageView = "pageview",
CopyShareCollectionLink = "Copy share collection link",
DownloadPublicCollection = "Download public collection",
}
40 changes: 40 additions & 0 deletions frontend/src/utils/analytics.ts
Original file line number Diff line number Diff line change
@@ -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);
}
3 changes: 3 additions & 0 deletions frontend/web-test-runner.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -55,6 +57,7 @@ export default {
ts: true,
tsconfig: fileURLToPath(new URL("./tsconfig.json", import.meta.url)),
target: "esnext",
define: defineConfig,
}),
commonjs({
include: [
Expand Down
10 changes: 2 additions & 8 deletions frontend/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions frontend/webpack.dev.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit ddf6fd4

Please sign in to comment.