From 5d7b11d64b53e6d868bbfb3c0a0c67c51005aae7 Mon Sep 17 00:00:00 2001 From: Daniel Schmidt Date: Mon, 20 Jul 2026 21:50:27 +0200 Subject: [PATCH 1/2] Rename analytics configuration section --- README.md | 20 +++++++++---------- .../appsettings.Development.json | 2 +- .../appsettings.json | 4 ++-- .../src/settings/connection-editor.element.ts | 4 ++-- .../settings/settings-dashboard.element.ts | 4 ++-- .../src/settings/settings-dashboard.test.ts | 4 ++-- .../VercelAnalyticsConnectionRegistry.cs | 3 ++- .../Configuration/VercelAnalyticsOptions.cs | 16 ++++++++++++--- .../WebAnalyticsSettingsApiController.cs | 3 ++- .../VercelAnalyticsOptionsValidatorTests.cs | 10 +++++----- 10 files changed, 41 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 3e3bd2a..9bdccc0 100644 --- a/README.md +++ b/README.md @@ -55,21 +55,21 @@ For a team-owned project, also copy either the team ID (`team_...`) or team slug Configure one shared token for the package: ```text -VercelAnalytics__AccessToken +WebAnalytics__Providers__Vercel__AccessToken ``` Examples: ```sh # Local shell or container environment -export VercelAnalytics__AccessToken="your_token" +export WebAnalytics__Providers__Vercel__AccessToken="your_token" # .NET user-secrets dotnet user-secrets init \ --project path/to/Your.Umbraco.Web.csproj dotnet user-secrets set \ - "VercelAnalytics:AccessToken" \ + "WebAnalytics:Providers:Vercel:AccessToken" \ "your_token" \ --project path/to/Your.Umbraco.Web.csproj ``` @@ -78,7 +78,7 @@ Use the equivalent secret/app-setting facility in Azure App Service, Kubernetes, Restart every Umbraco application instance after adding or rotating a token. Tokens are loaded from server configuration at application startup. -The shared token is used by every connection. If a project must use a different token, expand **Token override** for that connection and copy the generated environment-variable name. Overrides use `VercelAnalytics__ConnectionAccessTokens__{connection-guid}`. +The shared token is used by every connection. If a project must use a different token, expand **Token override** for that connection and copy the generated environment-variable name. Overrides use `WebAnalytics__ConnectionAccessTokens__{connection-guid}`. ### 3. Configure the connection in Umbraco @@ -121,7 +121,7 @@ The backoffice settings screen is the normal configuration path. A deployment ca ```json { - "VercelAnalytics": { + "WebAnalytics": { "Enabled": true, "DefaultRangeDays": 30, "CacheDuration": "00:05:00", @@ -147,12 +147,12 @@ The backoffice settings screen is the normal configuration path. A deployment ca ### Configuration reference -Package settings use the `VercelAnalytics` section. +Package settings use the `WebAnalytics` section. | Key | Default | Description | | --- | --- | --- | | `Enabled` | `false` | Enables the Analytics section and configured document workspace views. | -| `AccessToken` | Empty | Shared Vercel access token used by every connection. Supply through secret configuration. | +| `Providers:Vercel:AccessToken` | Empty | Shared Vercel access token used by every connection. Supply through secret configuration. | | `DefaultRangeDays` | `30` | Initial report range in days. Valid values are 1–730. | | `CacheDuration` | `00:05:00` | Per-instance in-memory cache duration. Valid values are zero to one hour. | | `Connections` | `[]` | Vercel project connection definitions. The first connection becomes the initial default. | @@ -171,7 +171,7 @@ Each entry under `Connections` supports: | `EnabledDocumentTypeKeys` | `[]` | Document-type GUIDs that show document analytics when all types are not enabled. | | `EnabledDocumentTypes` | `[]` | Document-type aliases used by configuration-only bootstrapping. Prefer stable document-type keys for settings managed in Umbraco. | -Keep tokens out of the JSON file. Supply the shared token through `VercelAnalytics__AccessToken`; only use `VercelAnalytics__ConnectionAccessTokens__{connection-guid}` when one connection requires an override. +Keep tokens out of the JSON file. Supply the shared token through `WebAnalytics__Providers__Vercel__AccessToken`; only use `WebAnalytics__ConnectionAccessTokens__{connection-guid}` when one connection requires an override. Before the settings screen has saved anything, Umbraco uses these server options as the initial configuration. After an administrator saves the settings screen, the non-secret settings are stored in Umbraco's database and become the source of truth. Access tokens continue to come from server configuration, with a connection-specific token taking precedence over the shared token. @@ -195,7 +195,7 @@ The available reporting window and some dimensions depend on the Vercel plan and | Symptom | Check | | --- | --- | -| **Token missing** | Configure `VercelAnalytics__AccessToken`, or a connection-specific override, and restart the application. | +| **Token missing** | Configure `WebAnalytics__Providers__Vercel__AccessToken`, or a connection-specific override, and restart the application. | | Vercel returns `401` or `403` | Confirm the token is valid, scoped to the owning account/team, and has access to the configured project. | | Vercel returns `400` | Verify the project ID and the optional `Team` value. | | Analytics section is not visible | Add the Analytics section to the user's Umbraco user group. The automatic administrator grant runs only once. | @@ -205,7 +205,7 @@ The available reporting window and some dimensions depend on the Vercel plan and ## Development -The example app opts into development data through `VercelAnalytics:EnableMockConnections` in its development settings. **Settings → Web Analytics** then includes presets for a full demo, UTM campaigns, feature flags, and custom events. Add and save a mock connection like any other connection, then select it in the Analytics dashboard. Mock reports are deterministic, require no access token, and never call Vercel. The package keeps mock connections disabled by default, including when it is consumed by another project running in Development. +The example app opts into development data through `WebAnalytics:EnableMockConnections` in its development settings. **Settings → Web Analytics** then includes presets for a full demo, UTM campaigns, feature flags, and custom events. Add and save a mock connection like any other connection, then select it in the Analytics dashboard. Mock reports are deterministic, require no access token, and never call Vercel. The package keeps mock connections disabled by default, including when it is consumed by another project running in Development. The client uses pnpm 11. From `src/TheBuilder.WebAnalytics/Client`: diff --git a/samples/TheBuilder.WebAnalytics.Example/appsettings.Development.json b/samples/TheBuilder.WebAnalytics.Example/appsettings.Development.json index f8cd66e..9b8c55b 100644 --- a/samples/TheBuilder.WebAnalytics.Example/appsettings.Development.json +++ b/samples/TheBuilder.WebAnalytics.Example/appsettings.Development.json @@ -17,7 +17,7 @@ } ] }, - "VercelAnalytics": { + "WebAnalytics": { "EnableMockConnections": true }, "Umbraco": { diff --git a/samples/TheBuilder.WebAnalytics.Example/appsettings.json b/samples/TheBuilder.WebAnalytics.Example/appsettings.json index 6377526..3da8d09 100644 --- a/samples/TheBuilder.WebAnalytics.Example/appsettings.json +++ b/samples/TheBuilder.WebAnalytics.Example/appsettings.json @@ -33,7 +33,7 @@ } } }, - "VercelAnalytics": { + "WebAnalytics": { "Enabled": true, "DefaultRangeDays": 30, "CacheDuration": "00:05:00", @@ -43,4 +43,4 @@ "umbracoDbDSN": "Data Source=|DataDirectory|/Umbraco.sqlite.db;Cache=Shared;Foreign Keys=True;Pooling=True", "umbracoDbDSN_ProviderName": "Microsoft.Data.Sqlite" } -} \ No newline at end of file +} diff --git a/src/TheBuilder.WebAnalytics/Client/src/settings/connection-editor.element.ts b/src/TheBuilder.WebAnalytics/Client/src/settings/connection-editor.element.ts index 4dadcb0..5050769 100644 --- a/src/TheBuilder.WebAnalytics/Client/src/settings/connection-editor.element.ts +++ b/src/TheBuilder.WebAnalytics/Client/src/settings/connection-editor.element.ts @@ -80,7 +80,7 @@ export class VercelAnalyticsConnectionEditorElement extends UmbElementMixin(LitE } async #copyTokenKey(): Promise { - await navigator.clipboard.writeText(`VercelAnalytics__ConnectionAccessTokens__${this.connection.key}`); + await navigator.clipboard.writeText(`WebAnalytics__ConnectionAccessTokens__${this.connection.key}`); this._tokenCopied = true; window.setTimeout(() => { this._tokenCopied = false; }, 2000); } @@ -164,7 +164,7 @@ export class VercelAnalyticsConnectionEditorElement extends UmbElementMixin(LitE Create a Vercel access token

-
VercelAnalytics__ConnectionAccessTokens__${connection.key}${this._tokenCopied ? "Copied" : "Copy"}
+
WebAnalytics__ConnectionAccessTokens__${connection.key}${this._tokenCopied ? "Copied" : "Copy"}
`} diff --git a/src/TheBuilder.WebAnalytics/Client/src/settings/settings-dashboard.element.ts b/src/TheBuilder.WebAnalytics/Client/src/settings/settings-dashboard.element.ts index e734705..ca25630 100644 --- a/src/TheBuilder.WebAnalytics/Client/src/settings/settings-dashboard.element.ts +++ b/src/TheBuilder.WebAnalytics/Client/src/settings/settings-dashboard.element.ts @@ -111,7 +111,7 @@ export class VercelAnalyticsSettingsDashboardElement extends UmbElementMixin(Lit } async #copyTokenKey(): Promise { - await navigator.clipboard.writeText("VercelAnalytics__AccessToken"); + await navigator.clipboard.writeText("WebAnalytics__Providers__Vercel__AccessToken"); this._tokenKeyCopied = true; window.setTimeout(() => { this._tokenKeyCopied = false; }, 2000); } @@ -267,7 +267,7 @@ export class VercelAnalyticsSettingsDashboardElement extends UmbElementMixin(Lit Create token
- VercelAnalytics__AccessToken + WebAnalytics__Providers__Vercel__AccessToken ${this._tokenKeyCopied ? "Copied" : "Copy"}
diff --git a/src/TheBuilder.WebAnalytics/Client/src/settings/settings-dashboard.test.ts b/src/TheBuilder.WebAnalytics/Client/src/settings/settings-dashboard.test.ts index 33b4bda..3ec3d56 100644 --- a/src/TheBuilder.WebAnalytics/Client/src/settings/settings-dashboard.test.ts +++ b/src/TheBuilder.WebAnalytics/Client/src/settings/settings-dashboard.test.ts @@ -159,7 +159,7 @@ describe("analytics settings onboarding", () => { const generatedKey = (editor as VercelAnalyticsConnectionEditorElement).connection.key; expect(generatedKey).toMatch(/^[0-9a-f-]{36}$/i); expect(editor?.shadowRoot?.querySelector(".token-key code")?.textContent) - .toBe(`VercelAnalytics__ConnectionAccessTokens__${generatedKey}`); + .toBe(`WebAnalytics__ConnectionAccessTokens__${generatedKey}`); expect(dashboard.shadowRoot?.querySelector(".unsaved-indicator")?.textContent?.trim()).toBe("Unsaved changes"); expect(dashboard.shadowRoot?.querySelector(".save-bar")).not.toBeNull(); expect(dashboard.shadowRoot?.querySelectorAll('[label="Save Web Analytics settings"]')).toHaveLength(1); @@ -182,7 +182,7 @@ describe("analytics settings onboarding", () => { document.body.append(dashboard); await vi.waitFor(() => expect(dashboard.shadowRoot?.querySelector(".shared-token")).not.toBeNull()); - expect(dashboard.shadowRoot?.querySelector(".shared-token code")?.textContent).toBe("VercelAnalytics__AccessToken"); + expect(dashboard.shadowRoot?.querySelector(".shared-token code")?.textContent).toBe("WebAnalytics__Providers__Vercel__AccessToken"); expect(dashboard.shadowRoot?.querySelector(".shared-token-status")?.textContent?.trim()).toBe("Not configured"); expect(dashboard.shadowRoot?.querySelector(".shared-token-help")?.textContent?.trim()).toBe( "Set this server environment variable to a Vercel access token.", diff --git a/src/TheBuilder.WebAnalytics/Configuration/VercelAnalyticsConnectionRegistry.cs b/src/TheBuilder.WebAnalytics/Configuration/VercelAnalyticsConnectionRegistry.cs index bb99e60..8e02ae2 100644 --- a/src/TheBuilder.WebAnalytics/Configuration/VercelAnalyticsConnectionRegistry.cs +++ b/src/TheBuilder.WebAnalytics/Configuration/VercelAnalyticsConnectionRegistry.cs @@ -74,6 +74,7 @@ internal RegistrySnapshot Capture() private RegistrySnapshot CreateSnapshot(VercelAnalyticsSettingsSnapshot settingsSnapshot) { var serverConfiguration = _serverOptions.Value; + var vercelConfiguration = serverConfiguration.Providers.Vercel; var connections = settingsSnapshot.Settings.Connections .Where(connection => !connection.IsMock || _mockConnectionsEnabled) .ToDictionary( @@ -81,7 +82,7 @@ private RegistrySnapshot CreateSnapshot(VercelAnalyticsSettingsSnapshot settings connection => VercelAnalyticsConnection.Create( connection, ResolveAccessToken( - serverConfiguration.AccessToken, + vercelConfiguration.AccessToken, serverConfiguration.ConnectionAccessTokens.GetValueOrDefault(connection.Key.ToString())))); var roots = connections.Values .SelectMany(connection => connection.DocumentRootKeys.Select(rootKey => (rootKey, connection.Key))) diff --git a/src/TheBuilder.WebAnalytics/Configuration/VercelAnalyticsOptions.cs b/src/TheBuilder.WebAnalytics/Configuration/VercelAnalyticsOptions.cs index 9767efc..85387e3 100644 --- a/src/TheBuilder.WebAnalytics/Configuration/VercelAnalyticsOptions.cs +++ b/src/TheBuilder.WebAnalytics/Configuration/VercelAnalyticsOptions.cs @@ -4,14 +4,12 @@ namespace TheBuilder.WebAnalytics.Configuration; public sealed class VercelAnalyticsOptions { - public const string SectionName = "VercelAnalytics"; + public const string SectionName = "WebAnalytics"; public bool Enabled { get; set; } = true; public bool EnableMockConnections { get; set; } - public string AccessToken { get; set; } = string.Empty; - public int DefaultRangeDays { get; set; } = 30; public TimeSpan CacheDuration { get; set; } = TimeSpan.FromMinutes(5); @@ -20,6 +18,18 @@ public sealed class VercelAnalyticsOptions public Dictionary ConnectionAccessTokens { get; set; } = new(StringComparer.OrdinalIgnoreCase); + + public WebAnalyticsProvidersOptions Providers { get; set; } = new(); +} + +public sealed class WebAnalyticsProvidersOptions +{ + public VercelAnalyticsProviderOptions Vercel { get; set; } = new(); +} + +public sealed class VercelAnalyticsProviderOptions +{ + public string AccessToken { get; set; } = string.Empty; } public sealed class VercelAnalyticsConnectionOptions diff --git a/src/TheBuilder.WebAnalytics/Controllers/WebAnalyticsSettingsApiController.cs b/src/TheBuilder.WebAnalytics/Controllers/WebAnalyticsSettingsApiController.cs index 1701d22..ca70de7 100644 --- a/src/TheBuilder.WebAnalytics/Controllers/WebAnalyticsSettingsApiController.cs +++ b/src/TheBuilder.WebAnalytics/Controllers/WebAnalyticsSettingsApiController.cs @@ -116,6 +116,7 @@ private async Task CreateResponseAsync(CancellationTo { var settings = settingsStore.Get(); var serverConfiguration = serverOptions.Value; + var vercelConfiguration = serverConfiguration.Providers.Vercel; var connections = registry.Connections.ToDictionary(connection => connection.Key); var responseTasks = settings.Connections.Select(async connection => { @@ -140,7 +141,7 @@ private async Task CreateResponseAsync(CancellationTo var responseConnections = await Task.WhenAll(responseTasks); return new AnalyticsSettingsResponse( settings.Enabled, - !string.IsNullOrWhiteSpace(serverConfiguration.AccessToken), + !string.IsNullOrWhiteSpace(vercelConfiguration.AccessToken), registry.MockConnectionsEnabled, settings.DefaultRangeDays, settings.CacheDuration.ToString("c"), diff --git a/tests/TheBuilder.WebAnalytics.Tests/Configuration/VercelAnalyticsOptionsValidatorTests.cs b/tests/TheBuilder.WebAnalytics.Tests/Configuration/VercelAnalyticsOptionsValidatorTests.cs index 3059535..0633fa2 100644 --- a/tests/TheBuilder.WebAnalytics.Tests/Configuration/VercelAnalyticsOptionsValidatorTests.cs +++ b/tests/TheBuilder.WebAnalytics.Tests/Configuration/VercelAnalyticsOptionsValidatorTests.cs @@ -34,7 +34,7 @@ public void Shared_token_is_valid_before_ui_setup() var options = new VercelAnalyticsOptions { Enabled = true, - AccessToken = "server-secret" + Providers = { Vercel = { AccessToken = "server-secret" } } }; Assert.True(_sut.Validate(null, options).Succeeded); @@ -98,7 +98,7 @@ public void Registry_does_not_throw_when_invalid_keys_bypass_startup_validation( public void Registry_uses_shared_access_token_when_connection_has_no_override() { var options = CreateOptions(); - options.AccessToken = "shared-token"; + options.Providers.Vercel.AccessToken = "shared-token"; var connection = new VercelAnalyticsConnectionRegistry(Options.Create(options)).Get(MainKey); @@ -111,7 +111,7 @@ public void Registry_uses_shared_access_token_when_connection_has_no_override() public void Registry_prefers_connection_access_token_override() { var options = CreateOptions(); - options.AccessToken = "shared-token"; + options.Providers.Vercel.AccessToken = "shared-token"; options.ConnectionAccessTokens[MainKey.ToString()] = "connection-token"; var connection = new VercelAnalyticsConnectionRegistry(Options.Create(options)).Get(MainKey); @@ -136,7 +136,7 @@ public void Connection_string_representation_redacts_access_token() public void Registry_is_unconfigured_when_shared_and_connection_tokens_are_missing() { var options = CreateOptions(); - options.AccessToken = string.Empty; + options.Providers.Vercel.AccessToken = string.Empty; var connection = new VercelAnalyticsConnectionRegistry(Options.Create(options)).Get(MainKey); @@ -210,7 +210,7 @@ public void Registry_supports_explicit_and_all_document_type_modes() private static VercelAnalyticsOptions CreateOptions() => new() { Enabled = true, - AccessToken = "test-token", + Providers = { Vercel = { AccessToken = "test-token" } }, Connections = [CreateConnection(MainKey)] }; From 1e9ba4fa6484eadacc6f9daec6ba3db2da4e0667 Mon Sep 17 00:00:00 2001 From: Daniel Schmidt Date: Mon, 20 Jul 2026 21:54:45 +0200 Subject: [PATCH 2/2] Fix analytics configuration test fixtures --- .../Configuration/VercelAnalyticsSettingsValidatorTests.cs | 2 +- .../Controllers/WebAnalyticsApiControllerTests.cs | 2 +- .../Services/AnalyticsDocumentRouteServiceTests.cs | 2 +- .../Services/VercelAnalyticsReportServiceTests.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/TheBuilder.WebAnalytics.Tests/Configuration/VercelAnalyticsSettingsValidatorTests.cs b/tests/TheBuilder.WebAnalytics.Tests/Configuration/VercelAnalyticsSettingsValidatorTests.cs index d2d4608..d9b1299 100644 --- a/tests/TheBuilder.WebAnalytics.Tests/Configuration/VercelAnalyticsSettingsValidatorTests.cs +++ b/tests/TheBuilder.WebAnalytics.Tests/Configuration/VercelAnalyticsSettingsValidatorTests.cs @@ -217,7 +217,7 @@ public void Store_preserves_connection_order_from_server_options() var options = new VercelAnalyticsOptions { Enabled = true, - AccessToken = "server-secret", + Providers = { Vercel = { AccessToken = "server-secret" } }, Connections = [ new() { Key = MainKey, ProjectId = "first-project" }, diff --git a/tests/TheBuilder.WebAnalytics.Tests/Controllers/WebAnalyticsApiControllerTests.cs b/tests/TheBuilder.WebAnalytics.Tests/Controllers/WebAnalyticsApiControllerTests.cs index a29d039..db29505 100644 --- a/tests/TheBuilder.WebAnalytics.Tests/Controllers/WebAnalyticsApiControllerTests.cs +++ b/tests/TheBuilder.WebAnalytics.Tests/Controllers/WebAnalyticsApiControllerTests.cs @@ -328,7 +328,7 @@ private static VercelAnalyticsConnectionRegistry EnabledRegistry(params string[] new(Options.Create(new VercelAnalyticsOptions { Enabled = true, - AccessToken = "secret", + Providers = { Vercel = { AccessToken = "secret" } }, Connections = (aliases.Length == 0 ? ["main"] : aliases).Select( alias => new VercelAnalyticsConnectionOptions { diff --git a/tests/TheBuilder.WebAnalytics.Tests/Services/AnalyticsDocumentRouteServiceTests.cs b/tests/TheBuilder.WebAnalytics.Tests/Services/AnalyticsDocumentRouteServiceTests.cs index 86119f5..a7e1df4 100644 --- a/tests/TheBuilder.WebAnalytics.Tests/Services/AnalyticsDocumentRouteServiceTests.cs +++ b/tests/TheBuilder.WebAnalytics.Tests/Services/AnalyticsDocumentRouteServiceTests.cs @@ -177,7 +177,7 @@ private static VercelAnalyticsConnectionRegistry CreateRegistry( params VercelAnalyticsConnectionOptions[] connections) => new(Options.Create(new VercelAnalyticsOptions { Enabled = true, - AccessToken = "secret", + Providers = { Vercel = { AccessToken = "secret" } }, Connections = connections.ToList() })); diff --git a/tests/TheBuilder.WebAnalytics.Tests/Services/VercelAnalyticsReportServiceTests.cs b/tests/TheBuilder.WebAnalytics.Tests/Services/VercelAnalyticsReportServiceTests.cs index 888986b..5f82610 100644 --- a/tests/TheBuilder.WebAnalytics.Tests/Services/VercelAnalyticsReportServiceTests.cs +++ b/tests/TheBuilder.WebAnalytics.Tests/Services/VercelAnalyticsReportServiceTests.cs @@ -424,7 +424,7 @@ public async Task Cancelling_one_summary_waiter_does_not_cancel_the_shared_clien private static VercelAnalyticsConnectionRegistry CreateRegistry(TimeSpan? cacheDuration = null) => new(Options.Create(new VercelAnalyticsOptions { Enabled = true, - AccessToken = "secret", + Providers = { Vercel = { AccessToken = "secret" } }, CacheDuration = cacheDuration ?? TimeSpan.FromMinutes(5), Connections = [