Added the "analytics.recorder" flag to the Website service.
This commit is contained in:
@@ -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)
|
||||
])
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user