Skip to content

Commit 8b9fc56

Browse files
committed
docs: update analytics documentation and refine OTel tracking implementation
1 parent 5e82e03 commit 8b9fc56

7 files changed

Lines changed: 194 additions & 34 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929

3030
- **Runtime**: Nginx-unprivileged on port 8080 (static site served by nginx)
3131
- **Build**: Gulp pipeline compiles SCSS, assembles HTML partials, and outputs to `dist/`
32-
- **Analytics**: Plausible integration for privacy-focused usage tracking
32+
- **Analytics**: OpenTelemetry browser instrumentation for usage analytics (logs exported via OTLP to Loki/Grafana)
3333
- **Health endpoint**: Nginx responds to probe requests on `/`
3434

3535
## Development Workflow
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
apiVersion: v1
2+
kind: ConfigMap
3+
metadata:
4+
name: {{ include "conversion-guide.fullname" . }}-nginx
5+
labels:
6+
{{- include "conversion-guide.labels" . | nindent 4 }}
7+
data:
8+
default.conf: |
9+
# ----------------------------------------
10+
# HTTP-level logging & helpers
11+
# ----------------------------------------
12+
13+
# Flag kubelet health probes by User-Agent
14+
map $http_user_agent $is_probe { default 0; ~kube-probe 1; }
15+
16+
# Flag error statuses
17+
map $status $is_error { default 0; ~^[45] 1; }
18+
19+
# Only log failing probe requests
20+
map "$is_probe:$is_error" $log_probe_fail { default 0; "1:1" 1; }
21+
22+
# Log all non-probe requests (browsers/APIs)
23+
map $is_probe $not_probe { 0 1; 1 0; }
24+
25+
# JSON access log for normal traffic
26+
log_format json escape=json
27+
'{'
28+
'"ts":"$time_iso8601",'
29+
'"remote":"$remote_addr",'
30+
'"method":"$request_method",'
31+
'"uri":"$request_uri",'
32+
'"status":$status,'
33+
'"bytes":$body_bytes_sent,'
34+
'"rt":$request_time,'
35+
'"ref":"$http_referer",'
36+
'"ua":"$http_user_agent",'
37+
'"req_id":"$request_id"'
38+
'}';
39+
40+
# Compact format for probe logs
41+
log_format probe '$remote_addr - $time_local "$request" $status rt=$request_time';
42+
43+
# Send error logs to stderr
44+
error_log /dev/stderr warn;
45+
46+
47+
# ----------------------------------------
48+
# Main server
49+
# ----------------------------------------
50+
server {
51+
listen 8080;
52+
server_name _;
53+
54+
root /usr/share/nginx/html;
55+
index index.html;
56+
57+
absolute_redirect off;
58+
port_in_redirect off;
59+
server_name_in_redirect off;
60+
61+
# Access logging:
62+
access_log /dev/stdout json if=$not_probe; # all non-probe requests
63+
access_log /dev/stdout probe if=$log_probe_fail; # ONLY failing probes
64+
65+
# Include MIME types
66+
include /etc/nginx/mime.types;
67+
68+
# On-the-fly compression for text-heavy responses
69+
gzip on;
70+
gzip_types text/plain text/css text/javascript application/javascript application/json image/svg+xml;
71+
gzip_min_length 256;
72+
gzip_vary on;
73+
74+
# Serve pre-compressed .gz files when available (forward-looking)
75+
gzip_static on;
76+
77+
# Serve clean URLs without .html extension
78+
location / {
79+
index index.html;
80+
try_files $uri $uri.html $uri/ =404;
81+
}
82+
83+
# Serve static assets
84+
location /assets/ {
85+
expires 30d;
86+
add_header Cache-Control "public, immutable";
87+
}
88+
89+
location /css/ {
90+
expires 1w;
91+
add_header Cache-Control "public, immutable";
92+
}
93+
94+
location /js/ {
95+
expires 1w;
96+
add_header Cache-Control "public, immutable";
97+
}
98+
99+
# OTel log ingestion proxy
100+
location = /v1/logs {
101+
proxy_pass http://{{ .Values.otel.collectorEndpoint }}/v1/logs;
102+
proxy_http_version 1.1;
103+
proxy_set_header Host $host;
104+
proxy_set_header X-Real-IP $remote_addr;
105+
proxy_set_header Content-Type $content_type;
106+
client_max_body_size 64k;
107+
access_log off;
108+
}
109+
110+
# Health endpoints (match probes)
111+
location = /healthz {
112+
access_log off;
113+
add_header Content-Type application/json;
114+
return 200 '{"status":"HEALTHY"}';
115+
}
116+
117+
location = /healthz/startup {
118+
access_log off;
119+
add_header Content-Type application/json;
120+
return 200 '{"status":"STARTUP_OK"}';
121+
}
122+
123+
location = /healthz/ready {
124+
access_log off;
125+
add_header Content-Type application/json;
126+
return 200 '{"status":"READY_OK"}';
127+
}
128+
129+
# Optional nice to haves
130+
sendfile on;
131+
keepalive_timeout 65s;
132+
}

charts/templates/deployment.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,15 @@ spec:
5757
port: http
5858
initialDelaySeconds: {{ .Values.probes.readiness.initialDelaySeconds }}
5959
periodSeconds: {{ .Values.probes.readiness.periodSeconds }}
60+
volumeMounts:
61+
- name: nginx-conf
62+
mountPath: /etc/nginx/conf.d
63+
readOnly: true
6064
{{- with .Values.resources }}
6165
resources:
6266
{{- toYaml . | nindent 12 }}
6367
{{- end }}
68+
volumes:
69+
- name: nginx-conf
70+
configMap:
71+
name: {{ include "conversion-guide.fullname" . }}-nginx

charts/values.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ zoneAntiAffinity:
4545
enabled: false
4646
topologyKey: topology.kubernetes.io/zone
4747

48+
otel:
49+
collectorEndpoint: "opentelemetry-collector.observability.svc:4318"
50+
4851
nodeSelector: {}
4952
tolerations: []
5053
affinity: {}

conf.d/default.conf

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,7 @@ server {
9090

9191
# OTel log ingestion proxy
9292
location = /v1/logs {
93-
resolver 127.0.0.11 valid=30s ipv6=off;
94-
set $otel_backend "opentelemetry-collector.observability.svc:4318";
95-
proxy_pass http://$otel_backend/v1/logs;
93+
proxy_pass http://opentelemetry-collector.observability.svc:4318/v1/logs;
9694
proxy_http_version 1.1;
9795
proxy_set_header Host $host;
9896
proxy_set_header X-Real-IP $remote_addr;

js/analytics/init.js

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,7 @@ function getSessionId() {
2929
function isProduction() {
3030
return (
3131
typeof window !== 'undefined' &&
32-
window.location.hostname !== 'localhost' &&
33-
window.location.hostname !== '127.0.0.1'
32+
window.location.hostname.endsWith('.ltc.bcit.ca')
3433
);
3534
}
3635

@@ -45,6 +44,8 @@ function getPageType() {
4544
return name;
4645
}
4746

47+
var _loggerProvider = null;
48+
4849
function init() {
4950
var prod = isProduction();
5051

@@ -61,12 +62,12 @@ function init() {
6162
? new BatchLogRecordProcessor(logExporter)
6263
: new SimpleLogRecordProcessor(logExporter);
6364

64-
var loggerProvider = new LoggerProvider({
65+
_loggerProvider = new LoggerProvider({
6566
resource,
6667
processors: [processor],
6768
});
6869

69-
logs.setGlobalLoggerProvider(loggerProvider);
70+
logs.setGlobalLoggerProvider(_loggerProvider);
7071

7172
registerInstrumentations({
7273
instrumentations: [
@@ -103,14 +104,28 @@ function init() {
103104
});
104105

105106
// Session heartbeat every 60s
106-
setInterval(function () {
107+
var heartbeatInterval = setInterval(function () {
107108
if (!document.hidden) {
108109
logEvent('session_heartbeat', {
109110
'duration_seconds': String(Math.round((Date.now() - startTime) / 1000)),
110111
...commonAttributes,
111112
});
112113
}
113114
}, 60000);
115+
116+
// Flush pending logs and emit session_end on tab close / navigate away
117+
document.addEventListener('visibilitychange', function () {
118+
if (document.visibilityState === 'hidden') {
119+
clearInterval(heartbeatInterval);
120+
logEvent('session_end', {
121+
'duration_seconds': String(Math.round((Date.now() - startTime) / 1000)),
122+
...commonAttributes,
123+
});
124+
if (_loggerProvider) {
125+
_loggerProvider.forceFlush();
126+
}
127+
}
128+
});
114129
}
115130

116131
function logEvent(eventName, attributes) {
@@ -147,4 +162,5 @@ init();
147162
window.otelAnalytics = {
148163
logEvent: logEvent,
149164
trackEvent: trackEvent,
165+
getPageType: getPageType,
150166
};

js/page-setup.js

Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -57,15 +57,13 @@
5757
$(this).toggleClass("open");
5858
var target = $(this).data("target");
5959
$(this).parents("section").find(target).trigger("button-pressed");
60-
if (window.otelAnalytics && typeof window.otelAnalytics.trackEvent === "function") {
61-
var viewMode = (target || "").replace(/^\./, "");
62-
var elementIndex = $(this).parents("section").index();
63-
window.otelAnalytics.trackEvent("view_toggle", {
64-
page_type: (window.location.pathname || "").split("/").filter(Boolean).pop() || "home",
65-
view_mode: viewMode,
66-
element_index: String(elementIndex)
67-
});
68-
}
60+
var viewMode = (target || "").replace(/^\./, "");
61+
var elementIndex = $(this).parents("section").index();
62+
trackEvent("view_toggle", {
63+
page_type: pageType,
64+
view_mode: viewMode,
65+
element_index: String(elementIndex)
66+
});
6967
});
7068

7169

@@ -302,20 +300,20 @@
302300
});
303301

304302
// Analytics tracking
305-
var pageType = getPageType();
303+
var pageType = (window.otelAnalytics && typeof window.otelAnalytics.getPageType === "function")
304+
? window.otelAnalytics.getPageType()
305+
: (function () {
306+
var path = window.location.pathname || "";
307+
var trimmed = path.replace(/\/+$/, "");
308+
var last = trimmed.split("/").filter(Boolean).pop() || "";
309+
var name = last.replace(/\.html$/, "");
310+
if (!name || name === "index") {
311+
return "home";
312+
}
313+
return name;
314+
}());
306315
var lastTrackedSection = null;
307316

308-
function getPageType() {
309-
var path = window.location.pathname || "";
310-
var trimmed = path.replace(/\/+$/, "");
311-
var last = trimmed.split("/").filter(Boolean).pop() || "";
312-
var name = last.replace(/\.html$/, "");
313-
if (!name || name === "index") {
314-
return "home";
315-
}
316-
return name;
317-
}
318-
319317
function trackEvent(eventName, attrs) {
320318
if (window.otelAnalytics && typeof window.otelAnalytics.trackEvent === "function") {
321319
window.otelAnalytics.trackEvent(eventName, attrs || {});
@@ -410,12 +408,17 @@
410408

411409
$(document).on("click", "a[href]", function () {
412410
var href = $(this).attr("href") || "";
411+
var isNav = $(this).closest(".menu, .nav-bar").length > 0;
412+
413413
var section = getContentSectionFromHref(href);
414414
if (section) {
415-
trackEvent("content_section", {
416-
page_type: pageType,
417-
content_section: section
418-
});
415+
lastTrackedSection = section;
416+
if (!isNav) {
417+
trackEvent("content_section", {
418+
page_type: pageType,
419+
content_section: section
420+
});
421+
}
419422
}
420423

421424
var assetInfo = getAssetInfo(href);

0 commit comments

Comments
 (0)