Pulse Analytics adds privacy-first analytics to an Umbraco site in one NuGet package. It sets no cookies and never stores IP addresses. The script it adds to your pages is under 3 KB.
Pulse Analytics is built by Ciphera, a company in Belgium, and hosted in Switzerland.
The package needs Umbraco 17 or later and targets .NET 10. You also need a Pulse Analytics account with a site registered for the same domain. The free plan is enough to start: it covers one site and 10,000 events a month, and you do not need a card.
See "Why Umbraco 17" below for how that floor was chosen and how to verify it is still current.
dotnet add package Ciphera.Pulse.UmbracoRegister the TagHelper, once, in Views/_ViewImports.cshtml:
@addTagHelper *, Ciphera.Pulse.UmbracoAdd the tag to your layout, once, anywhere inside <head>:
<head>
...
<pulse-analytics />
</head>That is the whole install. <pulse-analytics /> itself never reaches the
response. It renders the actual <script> tag(s) in its place, or nothing
at all (see "When it renders nothing" below).
Configuration lives in appsettings.json under a PulseAnalytics section, the
same place Our.Umbraco.TagHelpers and SeoToolkit.Umbraco keep their
settings. The settings are bound with IOptions and validated at startup. The
package has no backoffice settings screen; see "Why appsettings.json, not a
settings screen" below.
{
"PulseAnalytics": {
"Domain": "",
"Companion": false,
"TrackPreview": false,
"ApiOrigin": ""
}
}| Setting | Default | What it does |
|---|---|---|
Domain |
empty | The domain this site is registered under in Pulse. If it is empty, the tracker falls back to the browser's own hostname, which is right for a site served on one domain. |
Companion |
false |
Records clicks, copies and form submits. It loads a second, separate script, described below. |
TrackPreview |
false |
Also fires the tracker while a content editor previews unpublished content in the backoffice. It is off by default because a preview session is not the site's audience. |
ApiOrigin |
empty | Sends events through your own proxy origin. It takes a bare origin, not a full URL. |
A mistyped Domain or ApiOrigin fails the site's startup with a message
naming the offending value. The bad value is not accepted, so a broken tag
cannot ship quietly. See "A mistyped domain fails loudly, at startup" below for
why that is the right moment for an appsettings.json-configured package, and
why it is stricter than the other Pulse integrations' "rejected at save time".
What lands in your pages is the literal tag. It is written into the HTML itself, and no wrapper or bootstrap script is involved:
<script defer data-domain="example.com" src="https://js.ciphera.net/script.js"></script>The package adds no inline JavaScript, so a strict Content-Security-Policy needs
only script-src https://js.ciphera.net and
connect-src https://pulse-api.ciphera.net. A nonce or unsafe-inline is not
required.
The interaction capture is a second request on purpose. Companion: true
loads script.interactions.js as a separate file. The core script's size is a
published claim, and nothing is folded into it to make a feature look free.
When it renders nothing. <pulse-analytics /> suppresses its own output
in two cases: under Umbraco's own backoffice path (see "How the backoffice
exclusion works" below), and during a content-preview session unless
TrackPreview is true. Both are covered by this package's own unit tests;
scripts/verify.sh proves the backoffice case against a real install.
This package adds a TagHelper, <pulse-analytics />, that a developer
registers once and places once in their own layout. It does not use an
IComposer + ASP.NET Core middleware that rewrites the response body looking
for </head>.
That distinction matters because it was the open question this package started
from. Pulse's own cross-platform integration research had flagged "IComposer
- middleware rewriting the HTML response" as Umbraco's weakest technical claim in the whole research corpus: undocumented by Umbraco, and never confirmed to work. Before writing a line of this package, that approach was checked against what real, accepted Umbraco packages do, by reading their own source and marketplace listings:
Our.Umbraco.TagHelpers(a long-running, actively used community package) ships TagHelpers that a developer registers with@addTagHelper *, Our.Umbraco.TagHelpersin_ViewImports.cshtmland places by hand.SeoToolkit.Umbraco's Script Manager is the closest real precedent for "inject a third-party script like Google Tag Manager or Hotjar", and it does the same thing:@addTagHelper *, SeoToolkit.Umbraco.ScriptManager.Core, then<render-script position="HeadBottom"></render-script>in the layout.- Nothing found in that search does response-rewriting middleware for this
purpose. Umbraco's own "Creating a Package" documentation never shows
middleware either. Every
IComposerexample registers a service, a notification handler, or a collection entry, which is the ordinary, intended use ofIComposerand the one this package makes (seePulseAnalyticsComposer, which registersPulseAnalyticsOptionsand nothing else).
So this package follows the pattern that shipped Umbraco packages use. A
TagHelper is also the better choice on its own terms: a middleware
response-rewrite has to find </head> in bytes whose shape it does not control
(streamed output, an already-committed response, a </head> inside a comment
or a string literal), while a TagHelper is part of the render tree Umbraco
already understands, so it cannot "miss".
The exclusion rests on two guarantees:
- Structural. Umbraco's backoffice is a separate single-page application.
It does not render your site's Razor layout, or any Razor layout, so there is
no code path through which
<pulse-analytics />could appear there. This needs no code to be true. - An explicit path check, as defence in depth.
PulseAnalyticsTagHelperalso checks the current request againstIHostingEnvironment.GetBackOfficePath()(the same API Umbraco itself uses to know its own backoffice path,/umbracoby default) and suppresses output there too. Decompiling the real Umbraco 17.0.0 binary while buildingscripts/verify.shconfirmed that this resolvesToAbsolute("~/umbraco"), a literal path.GlobalSettingshas noUmbracoPathproperty in Umbraco 17 any more, so there is nothing to configure here today.
The second guarantee is not redundant. A Surface controller (Umbraco's own
base class for front-end form posts, such as a newsletter signup or a contact
form) is mounted under the backoffice's own path prefix
(/umbraco/surface/{controller}/{action}) by Umbraco's own routing convention,
even though its job is entirely front-end. A developer who shares one layout
across the whole site, including a surface controller's "thank you" page, is a
realistic case, and it is exactly the case where the structural guarantee alone
would not hold, because that view is part of the front-end Razor render tree.
scripts/verify.sh tests this directly: a surface controller sharing the same
layout as the public /verify page, asserting that the tag appears on one and
not the other, plus a --mutate mode that removes the guard and proves the
check can go red.
The Drupal, Joomla and TYPO3 Pulse integrations each add a backoffice/admin
settings screen, because that is how those platforms' developers expect to
configure a module. Umbraco's own convention for a package like this is
different: Our.Umbraco.TagHelpers and SeoToolkit.Umbraco both configure
through appsettings.json sections bound with IOptions, not a custom
backoffice section. Following that convention also keeps the value in source
control and in whatever secrets or config management the site already uses,
instead of in the database, and it leaves this package no backoffice UI to
maintain across major versions.
Every Pulse integration with a settings form rejects an invalid domain at the
moment it would be saved, so a broken value never reaches a live site. There
is no save button here, so the closest equivalent moment is application
startup: PulseAnalyticsComposer registers PulseAnalyticsOptionsValidator
with .ValidateOnStart(), so a malformed PulseAnalytics:Domain or
PulseAnalytics:ApiOrigin throws an OptionsValidationException that names the
bad value, before the site starts serving requests. This is a stricter choice
than the other integrations' "rejected at save time, not stored": an Umbraco
deploy fails in CI or at first boot and never silently in production. That is
the stronger form of the same house rule, "no silent failures", because an
analytics install that looks fine and reports nothing is the worst outcome this
package can produce.
Pulse Analytics counts pageviews, referrers, countries, devices, time on page and scroll depth, plus goals, funnels and campaigns. It sets no cookies and never stores IP addresses. It does not count visitors whose browser sends Do Not Track or Global Privacy Control. If you test in Brave or Firefox and see nothing, the tracker is behaving correctly. Safari is the easiest browser to verify an install in.
Umbraco ships a new major twice a year and designates roughly one a year as
Long-Term Support. Verified 14-10-2026, directly against umbraco.com and the
published NuGet package metadata, not a cached or remembered figure. Re-check
these before trusting them on a later date:
| Version | Released | Type | EOL | Targets |
|---|---|---|---|---|
| 13 | 14-12-2023 | LTS | 14-12-2026 | .NET 8 |
| 17 | 27-11-2025 | LTS | 27-11-2028 | .NET 10 |
| 18 | 25-06-2026 | STS | 25-06-2027 | .NET 10 |
Umbraco 13 LTS is still supported but reaches end of life on 14-12-2026, so it
is not the version to build a new package against. Umbraco 17 is the current
LTS ("17 and .NET 10 arrive as dual LTS releases", from umbraco.com's own 17
LTS announcement), and Umbraco 18 is the current latest major, released as a
Standard-Term Support version on the same .NET 10 base. The package targets
net10.0 (its only target framework) and depends on
Umbraco.Cms.Core >= 17.0.0, a minimum and not a pin (see the csproj comment).
That covers both 17 and 18 in one build, and the Umbraco Marketplace itself
derives the supported-version range from exactly that dependency (see
RELEASING.md). The real-install harness (scripts/verify.sh) passed on
Umbraco 17.8.1 and on Umbraco 18.3.1 on 09-11-2026.
Apache-2.0, the same licence as the Astro, Docusaurus, Framer, GTM and Nuxt
integrations. The licence is a free choice here: Drupal, Joomla and TYPO3 all
force GPL-2.0-or-later on anything they host, because each of those
platforms' own licensing policy treats an installed module, plugin or extension
as part of one combined GPL program. Umbraco does not. Umbraco CMS's core is
MIT-licensed (confirmed from umbraco.com: "the whole system has been
available under an MIT License" since version 4.5), and real, accepted Umbraco
Marketplace packages ship under a mix of licences with no forced convergence:
Our.Umbraco.TagHelpers and SeoToolkit.Umbraco are MIT, uSync is MPL-2.0.
Nothing in the Marketplace listing process ("Listing Your Package") requires a
particular OSS licence either. Its LicenseTypes field
(Free/Purchase/Subscription) is a pricing-tier label, not an SPDX licence.
scripts/verify.sh installs a real Umbraco site in Docker and reads the served
HTML. It has been run against a real install (see RELEASING.md). It does not
content-author a page through the backoffice. Instead it adds a small MVC
controller and a surface controller to the scaffolded site itself, both
rendering the site's own shared layout. This is the smallest honest substitute
for driving the backoffice's content-authoring UI, which varies by version and
has nothing to do with this package's own logic: the DI container,
configuration binding, TagHelper discovery and Razor rendering pipeline under
test are identical either way. It does not cover real content-authored pages or
a real preview session, because triggering InPreviewMode needs an actual
content node and the backoffice's own preview-issuing flow. The package's own
unit tests (PulseAnalyticsTagHelperTests) cover TrackPreview and the preview
exclusion instead, with a mocked IUmbracoContext.
Umbraco is a registered trademark of Umbraco A/S. This is an independent package and is not affiliated with or endorsed by Umbraco A/S or the Umbraco project.