Compare commits

...

6 Commits

Author SHA1 Message Date
Oliver Kieselbach 41b33895c5 bump pointer: internal (Mark as Succeeded ends a running agent, D-374)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:53:18 +02:00
Oliver Kieselbach 157273726d Mark as Succeeded ends a running agent, and the agent acts on an admin mark once (D-374)
The backend repeats AdminAction on every telemetry response while the session
row carries it. The agent handled each echo anew: a new admin_marked_session
with an immediate upload, whose response carried the next echo. With a running
agent on a Mark-Succeeded session that looped at the device rate limit until
the agent ended; Mark-Failed ended the agent and stayed bounded.

- ServerControlPlane: AdminActionLatch admits the first AdminAction of a
  process; every later one only logs at Debug.
- AdminAction=Succeeded runs the agent's own termination pipeline with outcome
  Succeeded, like Failed: what the portal's confirmation dialog and the docs
  promise, and what the restart path already did.
- HandleAdminActionTests: Succeeded terminates; repeated echoes, a later
  different outcome and concurrent callers are handled once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:51:49 +02:00
Oliver Kieselbach 0d994773d8 bump pointer: internal, .claude/commands (entry channel released)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 18:30:14 +02:00
Oliver Kieselbach 7dd9f81c31 bump pointer: internal, .claude/commands (entry channel, D-373)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 18:23:50 +02:00
Oliver Kieselbach 7d07cb7805 Marketing clicks and page views on www carry the channel their page load came in through (D-373)
On the public host a telemetry initializer adds the referrer's host and
the utm_source, utm_medium and utm_campaign tags of the entry URL to every
page view and custom event. Both come from the document itself
(document.referrer and the navigation entry), so nothing is stored and no
identifier is added; metrics, dependencies and the portal stay as they are.

The hero and final get-started buttons and the three Pro plan links now
navigate client-side, so landing, plans, get-started and the sign-in click
stay one page load. A guard fails on a plain <a> to /get-started or /plans.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 18:23:03 +02:00
Oliver Kieselbach 485a5b4604 bump pointer: internal (TPM-PSS precision released)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 13:08:15 +02:00
12 changed files with 388 additions and 73 deletions
@@ -3,6 +3,8 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AutopilotMonitor.Agent.V2.Core.Configuration;
using AutopilotMonitor.Agent.V2.Core.Logging;
using AutopilotMonitor.Agent.V2.Core.Orchestration;
@@ -16,14 +18,16 @@ using Xunit;
namespace AutopilotMonitor.Agent.V2.Core.Tests.Program
{
/// <summary>
/// Regression gate for the "soft admin-action" semantics change: backend
/// <c>AdminAction=Succeeded</c> is purely informational (timeline entry only, agent keeps
/// running its own path) while <c>AdminAction=Failed</c> drives the agent's own soft-shutdown
/// pipeline via the injected <c>onAdminFailed</c> callback — without forcing self-destruct.
/// Previously both were routed through the full <c>ServerActionDispatcher</c> +
/// <c>terminationHandler.Handle</c> path, which (a) hijacked the agent's own completion flow
/// for Succeeded and (b) crashed with <c>InvalidOperationException: SignalIngress has been
/// stopped</c> when a terminal-drain response arrived after the ingress was torn down.
/// Regression gate for the soft admin-action semantics: backend <c>AdminAction=Succeeded</c>
/// and <c>AdminAction=Failed</c> (portal Mark-Succeeded / Mark-Failed) both drive the agent's
/// own soft-shutdown pipeline via the injected <c>onAdminTerminal</c> callback with that
/// outcome — without forcing self-destruct — and only the first AdminAction of a process is
/// acted on. The backend repeats AdminAction on every telemetry response; acting on each echo
/// re-emitted <c>admin_marked_session</c> with an immediate upload whose response carried the
/// next echo. Earlier, both outcomes were routed through the full
/// <c>ServerActionDispatcher</c> path, which crashed with
/// <c>InvalidOperationException: SignalIngress has been stopped</c> when a terminal-drain
/// response arrived after the ingress was torn down.
/// </summary>
public sealed class HandleAdminActionTests
{
@@ -34,13 +38,16 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Program
ApiBaseUrl = "http://localhost",
};
private static global::AutopilotMonitor.Agent.V2.Runtime.ServerControlPlane.AdminActionLatch NewLatch() =>
new global::AutopilotMonitor.Agent.V2.Runtime.ServerControlPlane.AdminActionLatch();
private sealed class Rig : IDisposable
{
public TempDirectory Tmp { get; } = new TempDirectory();
public AgentLogger Logger { get; }
public FakeSignalIngressSink Ingress { get; } = new FakeSignalIngressSink();
public InformationalEventPost Post { get; }
public List<EnrollmentTerminatedEventArgs> OnAdminFailedCalls { get; } = new List<EnrollmentTerminatedEventArgs>();
public List<EnrollmentTerminatedEventArgs> OnAdminTerminalCalls { get; } = new List<EnrollmentTerminatedEventArgs>();
public Rig()
{
@@ -48,7 +55,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Program
Post = new InformationalEventPost(Ingress, new VirtualClock(new DateTime(2026, 4, 23, 10, 0, 0, DateTimeKind.Utc)));
}
public Action<EnrollmentTerminatedEventArgs> OnAdminFailed => args => OnAdminFailedCalls.Add(args);
public Action<EnrollmentTerminatedEventArgs> OnAdminTerminal => args => OnAdminTerminalCalls.Add(args);
public IEnumerable<FakeSignalIngressSink.PostedSignal> EventsOfType(string eventType) =>
Ingress.Posted.Where(p =>
@@ -61,15 +68,16 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Program
}
[Fact]
public void Succeeded_emits_admin_marked_session_as_Info_and_does_not_invoke_onAdminFailed()
public void Succeeded_emits_admin_marked_session_as_Info_and_terminates_with_Succeeded()
{
using var rig = new Rig();
global::AutopilotMonitor.Agent.V2.Runtime.ServerControlPlane.HandleAdminAction(
adminAction: "Succeeded",
stageNameAccessor: () => "Completed",
latch: NewLatch(),
stageNameAccessor: () => "EspAccountSetup",
post: rig.Post,
onAdminFailed: rig.OnAdminFailed,
onAdminTerminal: rig.OnAdminTerminal,
agentConfig: Config(),
logger: rig.Logger);
@@ -78,24 +86,29 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Program
Assert.True(emitted[0].Payload!.TryGetValue(SignalPayloadKeys.Severity, out var sev));
Assert.Equal(EventSeverity.Info.ToString(), sev);
// Informational only — agent keeps running its own path.
Assert.Empty(rig.OnAdminFailedCalls);
Assert.Single(rig.OnAdminTerminalCalls);
var args = rig.OnAdminTerminalCalls[0];
Assert.Equal(EnrollmentTerminationOutcome.Succeeded, args.Outcome);
Assert.Equal(EnrollmentTerminationReason.DecisionTerminalStage, args.Reason);
Assert.Equal("EspAccountSetup", args.StageName);
Assert.Contains("Admin marked session as Succeeded", args.Details);
// No server_action_* noise — Succeeded isn't a ServerAction.
// No server_action_* noise — an admin mark isn't a ServerAction.
Assert.Empty(rig.EventsOfType("server_action_received"));
Assert.Empty(rig.EventsOfType("server_action_executed"));
}
[Fact]
public void Failed_emits_admin_marked_session_as_Warning_and_invokes_onAdminFailed_once()
public void Failed_emits_admin_marked_session_as_Warning_and_terminates_with_Failed()
{
using var rig = new Rig();
global::AutopilotMonitor.Agent.V2.Runtime.ServerControlPlane.HandleAdminAction(
adminAction: "Failed",
latch: NewLatch(),
stageNameAccessor: () => "DeviceSetup",
post: rig.Post,
onAdminFailed: rig.OnAdminFailed,
onAdminTerminal: rig.OnAdminTerminal,
agentConfig: Config(),
logger: rig.Logger);
@@ -104,8 +117,8 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Program
Assert.True(emitted[0].Payload!.TryGetValue(SignalPayloadKeys.Severity, out var sev));
Assert.Equal(EventSeverity.Warning.ToString(), sev);
Assert.Single(rig.OnAdminFailedCalls);
var args = rig.OnAdminFailedCalls[0];
Assert.Single(rig.OnAdminTerminalCalls);
var args = rig.OnAdminTerminalCalls[0];
Assert.Equal(EnrollmentTerminationOutcome.Failed, args.Outcome);
Assert.Equal(EnrollmentTerminationReason.DecisionTerminalStage, args.Reason);
Assert.Equal("DeviceSetup", args.StageName);
@@ -119,13 +132,15 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Program
global::AutopilotMonitor.Agent.V2.Runtime.ServerControlPlane.HandleAdminAction(
adminAction: "succeeded",
latch: NewLatch(),
stageNameAccessor: () => null,
post: rig.Post,
onAdminFailed: rig.OnAdminFailed,
onAdminTerminal: rig.OnAdminTerminal,
agentConfig: Config(),
logger: rig.Logger);
Assert.Empty(rig.OnAdminFailedCalls);
Assert.Single(rig.OnAdminTerminalCalls);
Assert.Equal(EnrollmentTerminationOutcome.Succeeded, rig.OnAdminTerminalCalls[0].Outcome);
}
[Fact]
@@ -137,28 +152,30 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Program
global::AutopilotMonitor.Agent.V2.Runtime.ServerControlPlane.HandleAdminAction(
adminAction: "Quarantined",
latch: NewLatch(),
stageNameAccessor: () => null,
post: rig.Post,
onAdminFailed: rig.OnAdminFailed,
onAdminTerminal: rig.OnAdminTerminal,
agentConfig: Config(),
logger: rig.Logger);
Assert.Single(rig.OnAdminFailedCalls);
Assert.Equal(EnrollmentTerminationOutcome.Failed, rig.OnAdminFailedCalls[0].Outcome);
Assert.Single(rig.OnAdminTerminalCalls);
Assert.Equal(EnrollmentTerminationOutcome.Failed, rig.OnAdminTerminalCalls[0].Outcome);
}
[Fact]
public void Failed_with_null_onAdminFailed_does_not_throw()
public void Null_onAdminTerminal_does_not_throw()
{
// Defensive path: onAdminFailed can be null if the terminate handler wasn't wired yet.
// Defensive path: onAdminTerminal can be null if the terminate handler wasn't wired yet.
using var rig = new Rig();
var ex = Record.Exception(() =>
global::AutopilotMonitor.Agent.V2.Runtime.ServerControlPlane.HandleAdminAction(
adminAction: "Failed",
latch: NewLatch(),
stageNameAccessor: () => null,
post: rig.Post,
onAdminFailed: null!,
onAdminTerminal: null!,
agentConfig: Config(),
logger: rig.Logger));
@@ -176,14 +193,15 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Program
global::AutopilotMonitor.Agent.V2.Runtime.ServerControlPlane.HandleAdminAction(
adminAction: "Failed",
latch: NewLatch(),
stageNameAccessor: () => throw new InvalidOperationException("not started"),
post: rig.Post,
onAdminFailed: rig.OnAdminFailed,
onAdminTerminal: rig.OnAdminTerminal,
agentConfig: Config(),
logger: rig.Logger);
Assert.Single(rig.OnAdminFailedCalls);
Assert.Null(rig.OnAdminFailedCalls[0].StageName);
Assert.Single(rig.OnAdminTerminalCalls);
Assert.Null(rig.OnAdminTerminalCalls[0].StageName);
}
[Fact]
@@ -202,18 +220,22 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Program
var ex = Record.Exception(() =>
global::AutopilotMonitor.Agent.V2.Runtime.ServerControlPlane.HandleAdminAction(
adminAction: "Succeeded",
latch: NewLatch(),
stageNameAccessor: () => null,
post: stoppedPost,
onAdminFailed: rig.OnAdminFailed,
onAdminTerminal: rig.OnAdminTerminal,
agentConfig: Config(),
logger: rig.Logger));
Assert.Null(ex);
Assert.Empty(rig.OnAdminFailedCalls);
// A lost timeline entry never cancels the admin's decision.
Assert.Single(rig.OnAdminTerminalCalls);
Assert.Equal(EnrollmentTerminationOutcome.Succeeded, rig.OnAdminTerminalCalls[0].Outcome);
}
[Fact]
public void Failed_emit_race_still_invokes_onAdminFailed()
public void Failed_emit_race_still_terminates()
{
// If the timeline emit fails due to a stopped ingress, the agent's own soft-shutdown
// must still run — the admin's decision to Mark-Failed outranks a lost timeline entry.
@@ -225,14 +247,85 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Program
global::AutopilotMonitor.Agent.V2.Runtime.ServerControlPlane.HandleAdminAction(
adminAction: "Failed",
latch: NewLatch(),
stageNameAccessor: () => "DeviceSetup",
post: stoppedPost,
onAdminFailed: rig.OnAdminFailed,
onAdminTerminal: rig.OnAdminTerminal,
agentConfig: Config(),
logger: rig.Logger);
Assert.Single(rig.OnAdminFailedCalls);
Assert.Equal(EnrollmentTerminationOutcome.Failed, rig.OnAdminFailedCalls[0].Outcome);
Assert.Single(rig.OnAdminTerminalCalls);
Assert.Equal(EnrollmentTerminationOutcome.Failed, rig.OnAdminTerminalCalls[0].Outcome);
}
[Theory]
[InlineData("Succeeded")]
[InlineData("Failed")]
public void Repeated_echo_is_handled_once(string adminAction)
{
// The backend repeats AdminAction on every telemetry response. Each echo handled anew
// emitted another immediate-upload timeline entry whose response carried the next echo.
using var rig = new Rig();
var latch = NewLatch();
for (var i = 0; i < 3; i++)
{
global::AutopilotMonitor.Agent.V2.Runtime.ServerControlPlane.HandleAdminAction(
adminAction: adminAction,
latch: latch,
stageNameAccessor: () => "EspAccountSetup",
post: rig.Post,
onAdminTerminal: rig.OnAdminTerminal,
agentConfig: Config(),
logger: rig.Logger);
}
Assert.Single(rig.EventsOfType("admin_marked_session"));
Assert.Single(rig.OnAdminTerminalCalls);
}
[Fact]
public void First_admin_action_of_the_process_wins()
{
// The first mark already started the shutdown with its outcome; a later change of
// mind in the portal only updates the session row on the backend.
using var rig = new Rig();
var latch = NewLatch();
foreach (var adminAction in new[] { "Succeeded", "Failed" })
{
global::AutopilotMonitor.Agent.V2.Runtime.ServerControlPlane.HandleAdminAction(
adminAction: adminAction,
latch: latch,
stageNameAccessor: () => null,
post: rig.Post,
onAdminTerminal: rig.OnAdminTerminal,
agentConfig: Config(),
logger: rig.Logger);
}
var emitted = rig.EventsOfType("admin_marked_session").ToList();
Assert.Single(emitted);
Assert.True(emitted[0].Payload!.TryGetValue(SignalPayloadKeys.Severity, out var sev));
Assert.Equal(EventSeverity.Info.ToString(), sev);
Assert.Single(rig.OnAdminTerminalCalls);
Assert.Equal(EnrollmentTerminationOutcome.Succeeded, rig.OnAdminTerminalCalls[0].Outcome);
}
[Fact]
public void Latch_admits_exactly_one_of_concurrent_callers()
{
// Responses are raised on whichever thread ran the drain, so two can overlap.
var latch = NewLatch();
var admitted = 0;
Parallel.For(0, 64, _ =>
{
if (latch.TryEnter()) Interlocked.Increment(ref admitted);
});
Assert.Equal(1, admitted);
}
private sealed class ThrowingSink : ISignalIngressSink
@@ -92,14 +92,14 @@ namespace AutopilotMonitor.Agent.V2.Runtime
/// <c>terminate_session</c> <see cref="ServerAction"/> with
/// <c>forceSelfDestruct=true</c> and routes it through the dispatcher, overriding
/// local <c>SelfDestructOnComplete=false</c>.</item>
/// <item><b>AdminAction=Succeeded</b> — informational. Admin clicked Mark-Succeeded in
/// the portal. Emits a single <c>admin_marked_session</c> timeline entry and lets
/// the agent finish its own path.</item>
/// <item><b>AdminAction=Failed</b> — soft admin-driven shutdown. Calls the agent's own
/// <see cref="EnrollmentTerminationHandler.Handle"/> with <c>outcome=Failed</c>,
/// which runs the same analyzers / diagnostics / summary / self-destruct sequence
/// as a locally-detected failure. Self-destruct is governed by local
/// <c>SelfDestructOnComplete</c> — no forced override.</item>
/// <item><b>AdminAction=Succeeded|Failed</b> — soft admin-driven shutdown. Admin clicked
/// Mark-Succeeded / Mark-Failed in the portal. Calls the agent's own
/// <see cref="EnrollmentTerminationHandler.Handle"/> with that outcome, which runs the
/// same analyzers / diagnostics / summary / self-destruct sequence as a locally-detected
/// completion or failure. Self-destruct is governed by local
/// <c>SelfDestructOnComplete</c> — no forced override. The backend repeats AdminAction
/// on every response, so only the first one per process is acted on
/// (<see cref="HandleAdminAction"/>).</item>
/// <item><b>upload.Actions[]</b> — genuinely backend-queued <see cref="ServerAction"/>s
/// (<c>rotate_config</c>, <c>request_diagnostics</c>). Still routed through the
/// dispatcher because these are real actions, not admin-button echoes.</item>
@@ -130,6 +130,8 @@ namespace AutopilotMonitor.Agent.V2.Runtime
orchestrator.Transport.SessionGone += (sender, reason) =>
HandleSessionGone(reason, emergencyReporter, dispatcher, shutdown, shutdownComplete, logger);
var adminActionLatch = new AdminActionLatch();
orchestrator.Transport.ServerResponseReceived += (sender, upload) =>
{
if (shutdownComplete.IsSet)
@@ -163,17 +165,17 @@ namespace AutopilotMonitor.Agent.V2.Runtime
try { return orchestrator.CurrentState?.Stage.ToString(); }
catch (InvalidOperationException) { return null; }
};
Action<EnrollmentTerminatedEventArgs> onAdminFailed = args =>
Action<EnrollmentTerminatedEventArgs> onAdminTerminal = args =>
{
var handler = terminationHandlerAccessor();
if (handler == null)
{
logger.Warning("AdminAction=Failed received but terminationHandler not constructed yet — ignoring.");
logger.Warning($"AdminAction={args.Outcome} received but terminationHandler not constructed yet — ignoring.");
return;
}
handler.Handle(sender: null, args: args);
};
HandleAdminAction(upload.AdminAction, stageNameAccessor, post, onAdminFailed, agentConfig, logger);
HandleAdminAction(upload.AdminAction, adminActionLatch, stageNameAccessor, post, onAdminTerminal, agentConfig, logger);
}
if (upload.Actions != null && upload.Actions.Count > 0)
@@ -265,16 +267,30 @@ namespace AutopilotMonitor.Agent.V2.Runtime
/// Soft admin-override handling. Portal Mark-Succeeded / Mark-Failed is an echo of an
/// admin button click, not a ServerAction: it bypasses the dispatcher (no
/// <c>server_action_received / _executed</c> noise) and flows straight into the agent's
/// own timeline / termination pipeline.
/// own timeline / termination pipeline — one <c>admin_marked_session</c> entry, then
/// <paramref name="onAdminTerminal"/> with the admin's outcome.
/// <para>
/// The backend repeats AdminAction on every telemetry response while the session row
/// carries it, so <paramref name="latch"/> admits only the first one per process. Acting
/// on every echo looped: each entry is uploaded immediately, and its response carried
/// the next echo.
/// </para>
/// </summary>
internal static void HandleAdminAction(
string adminAction,
AdminActionLatch latch,
Func<string> stageNameAccessor,
InformationalEventPost post,
Action<EnrollmentTerminatedEventArgs> onAdminFailed,
Action<EnrollmentTerminatedEventArgs> onAdminTerminal,
AgentConfiguration agentConfig,
AgentLogger logger)
{
if (!latch.TryEnter())
{
logger.Debug($"Backend repeated AdminAction={adminAction} — already handled in this process, ignoring.");
return;
}
var outcome = string.Equals(adminAction, "Succeeded", StringComparison.OrdinalIgnoreCase)
? EnrollmentTerminationOutcome.Succeeded
: EnrollmentTerminationOutcome.Failed;
@@ -311,8 +327,7 @@ namespace AutopilotMonitor.Agent.V2.Runtime
logger.Warning($"admin_marked_session emit failed: {ex.Message}");
}
if (outcome != EnrollmentTerminationOutcome.Failed) return;
if (onAdminFailed == null) return;
if (onAdminTerminal == null) return;
string stageName = null;
try { stageName = stageNameAccessor?.Invoke(); }
@@ -320,19 +335,31 @@ namespace AutopilotMonitor.Agent.V2.Runtime
try
{
onAdminFailed(new EnrollmentTerminatedEventArgs(
onAdminTerminal(new EnrollmentTerminatedEventArgs(
reason: EnrollmentTerminationReason.DecisionTerminalStage,
outcome: EnrollmentTerminationOutcome.Failed,
outcome: outcome,
stageName: stageName,
terminatedAtUtc: DateTime.UtcNow,
details: "Admin marked session as Failed via portal."));
details: $"Admin marked session as {outcome} via portal."));
}
catch (Exception ex)
{
logger.Error("onAdminFailed (AdminAction=Failed) threw.", ex);
logger.Error($"onAdminTerminal (AdminAction={adminAction}) threw.", ex);
}
}
/// <summary>
/// Admits the first <see cref="HandleAdminAction"/> call of the process. Atomic: responses
/// are raised on whichever thread ran the drain — the drain loop or the terminal drain —
/// so two can arrive at once.
/// </summary>
internal sealed class AdminActionLatch
{
private int _entered;
public bool TryEnter() => Interlocked.Exchange(ref _entered, 1) == 0;
}
/// <summary>
/// Maps a <c>terminate_session</c> ServerAction's <c>adminOutcome</c> param onto an
/// <see cref="EnrollmentTerminationOutcome"/>. Portal Mark-Succeeded was previously
@@ -1,4 +1,5 @@
import type { Metadata } from "next";
import Link from "next/link";
import { LandingNavbar } from "../../components/landing/LandingNavbar";
import { SignupConsentCta } from "../../components/landing/SignupConsentCta";
import { SiteFooter } from "../../components/SiteFooter";
@@ -102,13 +103,14 @@ export default function GetStartedPage() {
<p className="mt-6 text-sm text-[var(--lp-ink-faint)] leading-relaxed max-w-xl">
When you need more later: the{" "}
<a
href="/plans/"
<Link
href="/plans"
prefetch={false}
data-track="get_started_pro_plan"
className="text-[var(--lp-accent-ink)] hover:opacity-80 underline"
>
Pro plan
</a>{" "}
</Link>{" "}
adds SLAs, support commitments, MSP delegation, higher operating limits and more —
same service, nothing to migrate.
</p>
@@ -1,5 +1,5 @@
import { describe, it, expect, vi } from "vitest";
import { readFileSync } from "node:fs";
import { readdirSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { createElement, type ComponentType } from "react";
import { renderToStaticMarkup } from "react-dom/server";
@@ -73,6 +73,18 @@ describe("marketing page tracking", () => {
expect(html).toMatch(/<section\b[^>]*\sid="push"[^>]*\sdata-track-section="push"/);
});
// A plain <a> loads a new document: the click's entry channel (lib/entryAttribution.ts) would
// turn into the own host. The static markup cannot tell a <Link> from an <a>, so the source is read.
it("links to get-started and plans are client-side Links, so the page load and its entry channel carry over", () => {
const root = join(__dirname, "..", "..");
const plain = ["components", "app"].flatMap(dir =>
readdirSync(join(root, dir), { encoding: "utf-8", recursive: true })
.filter(file => file.endsWith(".tsx") && !file.includes("__tests__"))
.filter(file => /<a\b[^>]*\shref="\/(?:get-started|plans)\b/.test(readFileSync(join(root, dir, file), "utf-8")))
);
expect(plain).toEqual([]);
});
it("the AI page carries every question text in its static HTML", () => {
const html = render(AiPage);
const escape = (text: string) => text.replace(/&/g, "&amp;").replace(/'/g, "&#x27;");
@@ -20,9 +20,9 @@ export function ClosingCta() {
Free, open source, deployed in minutes.
<br />
{"Need SLAs, support, or MSP delegation? "}
<a href="/plans/" data-track="closing_pro_plan" className="text-[var(--lp-accent-ink)] underline hover:opacity-80">
<Link href="/plans" prefetch={false} data-track="closing_pro_plan" className="text-[var(--lp-accent-ink)] underline hover:opacity-80">
There&apos;s a Pro plan
</a>
</Link>
.
</p>
</div>
@@ -1,3 +1,5 @@
import Link from "next/link";
export function FinalCta() {
return (
<section data-track-section="final_cta" className="py-20 sm:py-24 px-6 border-t border-[var(--lp-line-soft)]">
@@ -6,24 +8,26 @@ export function FinalCta() {
Your next enrollment doesn&apos;t have to be a black box.
</h2>
<div className="shrink-0 lg:text-right">
<a
href="/get-started/"
<Link
href="/get-started"
prefetch={false}
data-track="final_get_started"
className="inline-block px-7 py-3 rounded-lg bg-[var(--lp-accent-ink)] hover:brightness-110 hover:shadow-lg text-white font-semibold shadow-md transition-all"
>
Start monitoring now
</a>
</Link>
<p className="mt-3 text-sm text-[var(--lp-ink-faint)] lg:text-right">
Free, open source, deployed in minutes.
<span className="block mt-0.5">
Need SLAs, support, or MSP delegation?{" "}
<a
href="/plans/"
<Link
href="/plans"
prefetch={false}
data-track="final_pro_plan"
className="text-[var(--lp-accent-ink)] hover:opacity-80 underline"
>
There&apos;s a Pro plan
</a>
</Link>
.
</span>
</p>
@@ -1,3 +1,4 @@
import Link from "next/link";
import { HeroShots } from "./HeroShots";
import { DOCS_URL } from "@/lib/config";
@@ -50,13 +51,14 @@ export function Hero() {
</p>
<div className="shrink-0">
<div className="flex items-center gap-3">
<a
href="/get-started/"
<Link
href="/get-started"
prefetch={false}
data-track="hero_get_started"
className="px-6 py-3 rounded-lg bg-[var(--lp-accent-ink)] hover:brightness-110 hover:shadow-lg text-white font-semibold shadow-md transition-all"
>
Get started
</a>
</Link>
<a
href={DOCS_URL}
data-track="hero_docs"
@@ -0,0 +1,93 @@
import { describe, expect, it } from "vitest";
import { entryAttribution, stampEntryAttribution } from "../entryAttribution";
const PAGE = "https://www.example.test/";
describe("entry referrer", () => {
it.each([
["no referrer", "", "direct"],
["a search engine origin", "https://search.example.test/", "search.example.test"],
["a full URL, cut to its host", "https://blog.example.test/2026/10/post?ref=feed&id=42", "blog.example.test"],
["an Android app referrer", "android-app://com.example.reader/", "com.example.reader"],
["upper case", "https://Social.EXAMPLE.test/", "social.example.test"],
["a port", "http://intranet.example.test:8080/wiki/page", "intranet.example.test:8080"],
["an earlier page load on the own host", "https://www.example.test/plans/", "www.example.test"],
["no host", "about:blank", "unknown"],
["garbage", "not a url", "unknown"],
])("%s", (_name, referrer, expected) => {
expect(entryAttribution(referrer, PAGE).entryReferrer).toBe(expected);
});
it("caps a long host", () => {
const host = `${"a".repeat(120)}.example.test`;
expect(entryAttribution(`https://${host}/`, PAGE).entryReferrer).toBe(host.slice(0, 100));
});
});
describe("entry UTM tags", () => {
it("reads source, medium and campaign of the document URL", () => {
const entry = entryAttribution("", `${PAGE}?utm_source=social-net&utm_medium=social&utm_campaign=launch_2026.10`);
expect(entry).toEqual({
entryReferrer: "direct",
entryUtmSource: "social-net",
entryUtmMedium: "social",
entryUtmCampaign: "launch_2026.10",
});
});
it("leaves a missing tag out", () => {
expect(entryAttribution("", `${PAGE}get-started/?utm_source=newsletter`)).toEqual({
entryReferrer: "direct",
entryUtmSource: "newsletter",
});
expect(entryAttribution("", PAGE)).toEqual({ entryReferrer: "direct" });
});
it("never reads utm_content, utm_term or other parameters", () => {
const entry = entryAttribution("", `${PAGE}?utm_content=recipient-123&utm_term=abc&ref=x&utm_source=event`);
expect(entry).toEqual({ entryReferrer: "direct", entryUtmSource: "event" });
});
it("lower-cases and trims a tag", () => {
expect(entryAttribution("", `${PAGE}?utm_source=%20Social-Net%20`).entryUtmSource).toBe("social-net");
});
it.each([
["spaces inside", "summit+2026"],
["an address", "someone%40example.test"],
["an empty value", ""],
["a leading separator", "-launch"],
["too long", "a".repeat(65)],
])("reports a tag with %s as other", (_name, value) => {
expect(entryAttribution("", `${PAGE}?utm_campaign=${value}`).entryUtmCampaign).toBe("other");
});
it("keeps the referrer when the document URL cannot be parsed", () => {
expect(entryAttribution("https://search.example.test/", "not a url")).toEqual({ entryReferrer: "search.example.test" });
});
});
describe("stampEntryAttribution", () => {
const entry = { entryReferrer: "search.example.test", entryUtmSource: "newsletter" };
it.each([["PageviewData"], ["EventData"]])("stamps a %s item and keeps its data", (baseType) => {
const envelope = { baseType, data: { tenantId: "t", theme: "dark" } as Record<string, unknown> };
expect(stampEntryAttribution(envelope, entry)).toBe(true);
expect(envelope.data).toEqual({ tenantId: "t", theme: "dark", ...entry });
});
it("creates the data bag when the item has none", () => {
const envelope: { baseType: string; data?: Record<string, unknown> } = { baseType: "EventData" };
stampEntryAttribution(envelope, entry);
expect(envelope.data).toEqual(entry);
});
it.each([["MetricData"], ["RemoteDependencyData"], ["PageviewPerformanceData"], ["ExceptionData"], [undefined]])(
"leaves a %s item alone",
(baseType) => {
const envelope = { baseType, data: { theme: "dark" } as Record<string, unknown> };
expect(stampEntryAttribution(envelope, entry)).toBe(false);
expect(envelope.data).toEqual({ theme: "dark" });
}
);
});
@@ -6,6 +6,8 @@ import {
import { onCLS, onFCP, onINP, onLCP, onTTFB, type Metric } from "web-vitals";
import { API_BASE_URL, PORTAL_URL, SITE_URL } from "@/lib/config";
import type { TelemetryContext } from "./appInsights";
import { readEntryAttribution, stampEntryAttribution } from "./entryAttribution";
import { isOnPublicHost } from "./hostRouting";
import { normalizeRoute, toWebVitalTelemetry } from "./webVitals";
import { buildOwnOriginNonApiPatterns, shouldDropDependency, stripPageViewFragments } from "./webTelemetryExclusions";
@@ -78,6 +80,13 @@ export function createAppInsights(connectionString: string, context: TelemetryCo
appInsights.addTelemetryInitializer((envelope) => {
stripPageViewFragments(envelope);
});
// Public host: page views and custom events carry the channel this page load came in through.
const entry = isOnPublicHost() ? readEntryAttribution() : null;
if (entry) {
appInsights.addTelemetryInitializer((envelope) => {
stampEntryAttribution(envelope, entry);
});
}
appInsights.loadAppInsights();
// Returning false drops the item (D-277); undefined keeps it.
@@ -0,0 +1,73 @@
/**
* The channel a page load on the public host came in through: the referrer's host and the UTM
* tags of the URL the document was loaded with. Both stay the same for the whole page load —
* a client-side navigation changes neither `document.referrer` nor the navigation entry — so
* nothing is stored. Stamped on page views and custom events, counted per channel, never per
* person (D-304, D-305).
*/
export type EntryAttribution = Readonly<Record<string, string>>;
/** Only these three tags: `utm_content` and `utm_term` are where mail tools put recipient ids. */
const UTM_PROPERTIES = [
["utm_source", "entryUtmSource"],
["utm_medium", "entryUtmMedium"],
["utm_campaign", "entryUtmCampaign"],
] as const;
const UTM_VALUE = /^[a-z0-9][a-z0-9._-]{0,63}$/;
const MAX_REFERRER_HOST = 100;
/** `baseType` of the SDK items that carry the entry: page views and custom events. */
const STAMPED_BASE_TYPES: readonly string[] = ["PageviewData", "EventData"];
/** The referrer's host only, never its path or query; `direct` without a referrer. */
function referrerHost(referrer: string): string {
if (!referrer) return "direct";
try {
const host = new URL(referrer).host.toLowerCase();
return host ? host.slice(0, MAX_REFERRER_HOST) : "unknown";
} catch {
return "unknown";
}
}
export function entryAttribution(referrer: string, documentUrl: string): EntryAttribution {
const entry: Record<string, string> = { entryReferrer: referrerHost(referrer) };
let params: URLSearchParams;
try {
params = new URL(documentUrl).searchParams;
} catch {
return entry;
}
for (const [param, property] of UTM_PROPERTIES) {
const value = params.get(param);
if (value === null) continue;
const normalized = value.trim().toLowerCase();
entry[property] = UTM_VALUE.test(normalized) ? normalized : "other";
}
return entry;
}
/** This page load's entry, read in the browser. */
export function readEntryAttribution(): EntryAttribution {
const navigation = performance.getEntriesByType("navigation")[0];
return entryAttribution(document.referrer, navigation?.name || window.location.href);
}
/** The fields of an ITelemetryItem the stamp reads and writes. */
interface EnvelopeLike {
baseType?: string;
data?: Record<string, unknown>;
}
/**
* For `addTelemetryInitializer`: adds the entry to a page view or custom event, in place. Other
* item kinds are left alone. Returns true when it stamped, for the tests — the initializer must
* not pass it on, a returned false drops the item.
*/
export function stampEntryAttribution(envelope: EnvelopeLike, entry: EntryAttribution): boolean {
if (!envelope.baseType || !STAMPED_BASE_TYPES.includes(envelope.baseType)) return false;
envelope.data = Object.assign(envelope.data ?? {}, entry);
return true;
}