Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 30 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Umbraco Vercel Analytics

`Umbraco.VercelAnalytics` displays Vercel Web Analytics in the Umbraco 17 backoffice.
`Umbraco.VercelAnalytics` displays Vercel Web Analytics in the Umbraco 17 and 18 backoffice.

It provides:

Expand All @@ -13,7 +13,7 @@ The package reads analytics already collected by Vercel. It does not add Vercel

## Requirements

- Umbraco CMS 17.1 or later.
- Umbraco CMS 17.1 or later, up to (but not including) Umbraco 19.
- A Vercel project with [Web Analytics enabled and installed](https://vercel.com/docs/analytics/quickstart).
- A [Vercel access token](https://vercel.com/kb/guide/how-do-i-use-a-vercel-api-access-token) scoped to the personal account or team that owns the project.
- The Vercel project ID (`prj_...`).
Expand Down Expand Up @@ -209,8 +209,34 @@ pnpm test
pnpm build
```

Run the example host before regenerating the OpenAPI client, then pass its Swagger URL to:
The generated API client is checked in with the package source. The example host registers the package's OpenAPI document for development without adding version-specific OpenAPI dependencies to the distributed package.

Run the example host against the Umbraco version whose document you want to use:

```sh
# Umbraco 17
dotnet run \
--project samples/Umbraco.VercelAnalytics.Example \
-p:UmbracoVersion=17.1.0

# Umbraco 18
dotnet run \
--project samples/Umbraco.VercelAnalytics.Example \
-p:UmbracoVersion=18.0.0
```

Use a separate database for each major when switching the example host between versions. Umbraco upgrades its database schema and does not support downgrading that database to an earlier major.

Then regenerate the client from the matching development endpoint:

```sh
pnpm generate-client -- <swagger-url>
cd src/Umbraco.VercelAnalytics/Client

# Umbraco 17
corepack pnpm generate-client -- \
https://localhost:44389/umbraco/swagger/umbracovercelanalytics/swagger.json

# Umbraco 18
corepack pnpm generate-client -- \
https://localhost:44389/umbraco/openapi/umbracovercelanalytics.json
```
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<CompressionEnabled>false</CompressionEnabled> <!-- Disable compression. E.g. for umbraco backoffice files. These files should be precompressed by node and not let dotnet handle it -->
<Nullable>enable</Nullable>
<CompressionEnabled>false</CompressionEnabled> <!-- Disable compression. E.g. for umbraco backoffice files. These files should be precompressed by node and not let dotnet handle it -->
<UmbracoVersion Condition="'$(UmbracoVersion)' == ''">17.1.0</UmbracoVersion>
<DefineConstants Condition="$([MSBuild]::VersionGreaterThanOrEquals('$(UmbracoVersion)', '18.0.0'))">$(DefineConstants);UMBRACO_18_OR_LATER</DefineConstants>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Umbraco.Cms" Version="17.1.0" />
<PackageReference Include="Umbraco.Cms.DevelopmentMode.Backoffice" Version="17.1.0" />
<PackageReference Include="Umbraco.Cms" Version="$(UmbracoVersion)" />
<PackageReference Include="Umbraco.Cms.DevelopmentMode.Backoffice" Version="$(UmbracoVersion)" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#if UMBRACO_18_OR_LATER
using Microsoft.AspNetCore.Mvc.Controllers;
using Umbraco.Cms.Api.Common.OpenApi;
using Umbraco.Cms.Api.Management.OpenApi;
#else
using Asp.Versioning;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi;
using Swashbuckle.AspNetCore.SwaggerGen;
using Umbraco.Cms.Api.Common.OpenApi;
using Umbraco.Cms.Api.Management.OpenApi;
#endif
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.VercelAnalytics;

namespace Umbraco.VercelAnalytics.Example;

public sealed class VercelAnalyticsOpenApiComposer : IComposer
{
public void Compose(IUmbracoBuilder builder)
{
#if UMBRACO_18_OR_LATER
builder.AddBackOfficeOpenApiDocument(
Constants.ApiName,
document => document
.WithTitle("Umbraco Vercel Analytics Backoffice API")
.WithBackOfficeAuthentication()
.ConfigureOpenApiOptions(options => options.AddOperationTransformer(
(operation, context, _) =>
{
if (context.Description.ActionDescriptor is ControllerActionDescriptor controller &&
controller.ControllerTypeInfo.Namespace?.StartsWith(
"Umbraco.VercelAnalytics.Controllers",
StringComparison.InvariantCultureIgnoreCase) is true)
{
operation.OperationId = context.Description.ActionDescriptor.RouteValues["action"];
}

return Task.CompletedTask;
})));
#else
builder.Services.AddSingleton<IOperationIdHandler, VercelAnalyticsOperationIdHandler>();
builder.Services.Configure<SwaggerGenOptions>(options =>
{
options.SwaggerDoc(
Constants.ApiName,
new OpenApiInfo
{
Title = "Umbraco Vercel Analytics Backoffice API",
Version = "1.0",
});
options.OperationFilter<VercelAnalyticsOperationSecurityFilter>();
});
#endif
}

#if !UMBRACO_18_OR_LATER
private sealed class VercelAnalyticsOperationSecurityFilter : BackOfficeSecurityRequirementsOperationFilterBase
{
protected override string ApiName => Constants.ApiName;
}

private sealed class VercelAnalyticsOperationIdHandler(IOptions<ApiVersioningOptions> apiVersioningOptions)
: OperationIdHandler(apiVersioningOptions)
{
protected override bool CanHandle(
ApiDescription apiDescription,
ControllerActionDescriptor controllerActionDescriptor)
=> controllerActionDescriptor.ControllerTypeInfo.Namespace?.StartsWith(
"Umbraco.VercelAnalytics.Controllers",
StringComparison.InvariantCultureIgnoreCase) is true;

public override string Handle(ApiDescription apiDescription)
=> $"{apiDescription.ActionDescriptor.RouteValues["action"]}";
}
#endif
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ const swaggerUrl = process.argv.slice(2).find((argument) => argument !== '--');
if (swaggerUrl === undefined) {
console.error(chalk.red(`ERROR: Missing URL to OpenAPI spec`));
console.error(`Please provide the URL to the OpenAPI spec as the first argument found in ${chalk.yellow('package.json')}`);
console.error(`Example: node generate-openapi.js ${chalk.yellow('https://localhost:44331/umbraco/swagger/REPLACE_ME/swagger.json')}`);
console.error(`Umbraco 17 example: node generate-openapi.js ${chalk.yellow('https://localhost:44389/umbraco/swagger/umbracovercelanalytics/swagger.json')}`);
console.error(`Umbraco 18 example: node generate-openapi.js ${chalk.yellow('https://localhost:44389/umbraco/openapi/umbracovercelanalytics.json')}`);
process.exit();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,8 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi;
using Swashbuckle.AspNetCore.SwaggerGen;
using Umbraco.Cms.Core.Composing;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Api.Management.OpenApi;
using Umbraco.Cms.Api.Common.OpenApi;
using Umbraco.VercelAnalytics.Configuration;
using Umbraco.VercelAnalytics.Services;

Expand Down Expand Up @@ -40,57 +33,6 @@ public void Compose(IUmbracoBuilder builder)
builder.Services.AddTransient<IAnalyticsPublishedContentAccessor, UmbracoAnalyticsPublishedContentAccessor>();
builder.Services.AddTransient<IAnalyticsDocumentRouteService, AnalyticsDocumentRouteService>();
builder.AddNotificationAsyncHandler<UmbracoApplicationStartedNotification, AnalyticsSectionAccessInitializer>();
builder.Services.AddSingleton<IOperationIdHandler, CustomOperationHandler>();

builder.Services.Configure<SwaggerGenOptions>(opt =>
{
// Related documentation:
// https://docs.umbraco.com/umbraco-cms/tutorials/creating-a-backoffice-api
// https://docs.umbraco.com/umbraco-cms/tutorials/creating-a-backoffice-api/adding-a-custom-swagger-document
// https://docs.umbraco.com/umbraco-cms/tutorials/creating-a-backoffice-api/versioning-your-api
// https://docs.umbraco.com/umbraco-cms/tutorials/creating-a-backoffice-api/access-policies

// Configure the Swagger generation options
// Add in a new Swagger API document solely for our own package that can be browsed via Swagger UI
// Along with having a generated swagger JSON file that we can use to auto generate a TypeScript client
opt.SwaggerDoc(Constants.ApiName, new OpenApiInfo
{
Title = "Umbraco Vercel Analytics Backoffice API",
Version = "1.0",
// Contact = new OpenApiContact
// {
// Name = "Some Developer",
// Email = "you@company.com",
// Url = new Uri("https://company.com")
// }
});

// Enable Umbraco authentication for the "Example" Swagger document
// PR: https://github.com/umbraco/Umbraco-CMS/pull/15699
opt.OperationFilter<UmbracoVercelAnalyticsOperationSecurityFilter>();
});
}

public class UmbracoVercelAnalyticsOperationSecurityFilter : BackOfficeSecurityRequirementsOperationFilterBase
{
protected override string ApiName => Constants.ApiName;
}

// This is used to generate nice operation IDs in our swagger json file
// So that the gnerated TypeScript client has nice method names and not too verbose
// https://docs.umbraco.com/umbraco-cms/tutorials/creating-a-backoffice-api/umbraco-schema-and-operation-ids#operation-ids
public class CustomOperationHandler : OperationIdHandler
{
public CustomOperationHandler(IOptions<ApiVersioningOptions> apiVersioningOptions) : base(apiVersioningOptions)
{
}

protected override bool CanHandle(ApiDescription apiDescription, ControllerActionDescriptor controllerActionDescriptor)
{
return controllerActionDescriptor.ControllerTypeInfo.Namespace?.StartsWith("Umbraco.VercelAnalytics.Controllers", comparisonType: StringComparison.InvariantCultureIgnoreCase) is true;
}

public override string Handle(ApiDescription apiDescription) => $"{apiDescription.ActionDescriptor.RouteValues["action"]}";
}
}
}
}
}
}
13 changes: 7 additions & 6 deletions src/Umbraco.VercelAnalytics/Umbraco.VercelAnalytics.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<StaticWebAssetBasePath>/</StaticWebAssetBasePath>
<StaticWebAssetBasePath>/</StaticWebAssetBasePath>
<UmbracoVersion Condition="'$(UmbracoVersion)' == ''">[17.1.0,19.0.0)</UmbracoVersion>
</PropertyGroup>

<PropertyGroup>
<PackageId>Umbraco.VercelAnalytics</PackageId>
<Product>Umbraco.VercelAnalytics</Product>
<Title>Umbraco.VercelAnalytics</Title>
<Version>0.1.0</Version>
<Description>Vercel Web Analytics reports inside the Umbraco 17 backoffice.</Description>
<Description>Vercel Web Analytics reports inside the Umbraco 17 and 18 backoffice.</Description>
<PackageTags>umbraco;vercel;analytics;backoffice</PackageTags>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageReadmeFile>README.md</PackageReadmeFile>
Expand All @@ -24,10 +25,10 @@


<ItemGroup>
<PackageReference Include="Umbraco.Cms.Web.Website" Version="17.1.0" />
<PackageReference Include="Umbraco.Cms.Web.Common" Version="17.1.0" />
<PackageReference Include="Umbraco.Cms.Api.Common" Version="17.1.0" />
<PackageReference Include="Umbraco.Cms.Api.Management" Version="17.1.0" />
<PackageReference Include="Umbraco.Cms.Web.Website" Version="$(UmbracoVersion)" />
<PackageReference Include="Umbraco.Cms.Web.Common" Version="$(UmbracoVersion)" />
<PackageReference Include="Umbraco.Cms.Api.Common" Version="$(UmbracoVersion)" />
<PackageReference Include="Umbraco.Cms.Api.Management" Version="$(UmbracoVersion)" />
</ItemGroup>

<ItemGroup>
Expand Down