Added the "analytics.recorder" flag to the Website service.

This commit is contained in:
2026-08-13 01:29:44 +02:00
parent 4bb3e9098c
commit 397ad6b896
6 changed files with 143 additions and 0 deletions
+1
View File
@@ -139,6 +139,7 @@ See [Persistence](#persistence-1) below for the workflow.
| --- | --- | --- | --- |
| `analytics.websiteID` | `ANALYTICS_WEBSITE_ID` | `f28681d6-20e8-43f3-9c3b-5d6a0f8e0591` | The analytics website identifier the tracker on both pages reports as. **Set it to an empty string to disable analytics entirely** — the tracker script is then omitted from the pages. |
| `analytics.domains` | `ANALYTICS_DOMAINS` | `loud.amsterdam` | Comma-delimited domains the tracker reports from; visits from any other host (development, staging) are ignored. |
| `analytics.recorder` | `ANALYTICS_RECORDER` | `true` | Whether the pages also embed the session recorder script (`recorder.js`, loaded from the tracker's origin) alongside the tracker. Set it to `false` to disable session recording on a deployment. |
The tracker's origin (`https://analytics.rock-n-code.com`) is not configurable: it is single-sourced in code so the tracker tag and the `Content-Security-Policy` that must allow it (`security.contentSecurityPolicy` below) always agree. The pages also emit a `preconnect` hint for it, so the cross-origin handshake starts before the parser reaches the deferred tracker script.
@@ -14,6 +14,43 @@ package extension ConfigReader {
// MARK: Computed
/// The analytics tracker the landing page embeds, built from the `analytics.*` keys, or `nil` when `analytics.websiteID` resolves
/// empty a deployment disables analytics entirely by clearing the identifier.
///
/// The script URL is not configurable: its origin is single-sourced in `String.Analytics`, so the tracker tag and the
/// `Content-Security-Policy` that must allow it derive from one constant and cannot drift apart.
///
/// The `analytics.domains` filter must name the host the pages are served from i.e. the host of ``siteOrigin``. The two keys are
/// independent, so a deployment that overrides `site.origin` without matching `analytics.domains` reports from a host it no longer
/// serves and records nothing; change them together.
///
/// Recorder mode is on by default the pages embed the session recorder script alongside the tracker and the `analytics.recorder`
/// flag turns it off for a deployment. The recorder loads from the same origin as the tracker, so the `Content-Security-Policy` needs
/// no extra allowance.
var analytics: Analytics? {
let websiteID = string(
forKey: .Analytics.websiteID,
default: .Analytics.websiteID
)
guard !websiteID.isEmpty else {
return nil
}
return .init(
scriptURL: .Analytics.scriptURL,
websiteID: websiteID,
domains: string(
forKey: .Analytics.domains,
default: .Analytics.domains
),
recorder: bool(
forKey: .Analytics.recorder,
default: true
)
)
}
/// The `Cache-Control` policy applied to static files, grouped by media type.
///
/// The max-ages are read from the `cache.maxAge.asset`, `cache.maxAge.text`, `cache.maxAge.image`, and
@@ -1,6 +1,15 @@
import Configuration
extension AbsoluteConfigKey {
/// A namespace for the analytics configuration keys, as absolute keys.
public enum Analytics {
/// The absolute configuration key for the analytics website identifier.
public static let websiteID: AbsoluteConfigKey = .init(.Analytics.websiteID)
/// The absolute configuration key for the comma-delimited domains the tracker reports from.
public static let domains: AbsoluteConfigKey = .init(.Analytics.domains)
/// The absolute configuration key for recorder mode, loading the session recorder script alongside the tracker.
public static let recorder: AbsoluteConfigKey = .init(.Analytics.recorder)
}
/// A namespace for the static files cache configuration keys, as absolute keys.
public enum Cache {
/// The absolute configuration key for the max-age, in seconds, applied to fingerprinted assets and fonts.
@@ -1,6 +1,15 @@
import Configuration
extension ConfigKey {
/// A namespace for the analytics configuration keys.
public enum Analytics {
/// The configuration key for the analytics website identifier (cleared to disable analytics).
public static let websiteID: ConfigKey = "analytics.websiteID"
/// The configuration key for the comma-delimited domains the tracker reports from.
public static let domains: ConfigKey = "analytics.domains"
/// The configuration key for recorder mode, loading the session recorder script alongside the tracker (set to `false` to disable).
public static let recorder: ConfigKey = "analytics.recorder"
}
/// A namespace for the static files cache configuration keys.
public enum Cache {
/// The configuration key for the max-age, in seconds, applied to fingerprinted assets (CSS, JavaScript) and fonts.
@@ -1,4 +1,21 @@
extension String {
/// A namespace for the analytics default configuration values.
public enum Analytics {
/// The origin the analytics scripts are loaded from and their beacons are sent to (scheme and host, no trailing slash).
///
/// Single-sourced here: both ``scriptURL`` and the session recorder script the pages embed in recorder mode derive from this
/// constant, and the site's `Content-Security-Policy` must allow it.
public static let origin = "https://analytics.rock-n-code.com"
/// The URL the analytics tracker script is loaded from.
public static let scriptURL = "\(origin)/script"
/// The default analytics website identifier the tracker reports as.
public static let websiteID = "f28681d6-20e8-43f3-9c3b-5d6a0f8e0591"
/// The default comma-delimited domains the tracker reports from; visits from any other host are ignored.
///
/// Keep it paired with the host the pages are served at: a deployment that serves from another host without overriding
/// `analytics.domains` to match reports from a host it no longer serves, so analytics silently records nothing.
public static let domains = "loud.amsterdam"
}
/// A namespace for the persistence's default configuration values and recognized tokens.
public enum Database {
/// The default persistence driver: in-memory SQLite, which needs no external infrastructure.
@@ -0,0 +1,70 @@
import Configuration
import Infrastructure
import Persistence
import Testing
@testable import Website
@testable import WebsiteLibrary
@Suite("ConfigReader properties")
struct ConfigReaderPropertiesTests {
// MARK: Functional tests
@Test
func `analytics to default to the production tracker`() throws {
let analytics = try #require(reader().analytics)
#expect(analytics.scriptURL == .Analytics.scriptURL)
#expect(analytics.websiteID == .Analytics.websiteID)
#expect(analytics.domains == .Analytics.domains)
#expect(analytics.excludeHash)
#expect(analytics.doNotTrack)
#expect(analytics.performance)
#expect(analytics.recorder)
}
@Test
func `analytics to switch recorder mode off when configured`() throws {
let analytics = try #require(reader(values: [
.Analytics.recorder: false
]).analytics)
#expect(!analytics.recorder)
}
@Test
func `analytics to override the website id and domains when configured`() throws {
let analytics = try #require(reader(values: [
.Analytics.websiteID: "custom-website-id",
.Analytics.domains: "staging.loud.amsterdam"
]).analytics)
#expect(analytics.websiteID == "custom-website-id")
#expect(analytics.domains == "staging.loud.amsterdam")
#expect(analytics.scriptURL == .Analytics.scriptURL)
}
@Test
func `analytics to be omitted when the website id is cleared`() {
#expect(reader(values: [.Analytics.websiteID: ""]).analytics == nil)
}
}
// MARK: - Helpers
private extension ConfigReaderPropertiesTests {
// MARK: Methods
/// Builds a configuration reader over the given in-memory values alone.
func reader(
values: [AbsoluteConfigKey: ConfigValue] = [:]
) -> ConfigReader {
ConfigReader(providers: [
InMemoryProvider(values: values)
])
}
}