Formalize v6 paid-surface classification

This commit is contained in:
rcourtman
2026-04-23 14:53:41 +01:00
parent 1bf42337c2
commit a5687cd22e
21 changed files with 131 additions and 56 deletions
+31
View File
@@ -75,6 +75,37 @@ Migration policy:
Support rule:
- If any self-hosted v6 install shows a bounded monitored-system cap after activation or migration, treat it as a bug rather than as intended policy. Guest limits still follow the active tier or continuity contract.
## V6 Product Classification
Pulse keeps some entitlement keys for compatibility, but not every Pro
capability key is a primary v6 product pillar.
### Build On In v6
These are the current self-hosted Pro pillars that Pulse should keep
investing in, surfacing, and marketing:
- Alert-triggered root-cause analysis.
- Safe remediation through Patrol auto-fix and higher-autonomy controls.
- 90-day history.
- Included team/admin extras: Advanced SSO (SAML), RBAC, audit logging,
reporting, and agent profiles.
### Compatibility-Only In v6
These remain valid runtime gates for backwards compatibility, but should not
be elevated into headline Pro marketing or generic upgrade prompts:
- `FeatureKubernetesAI` / `kubernetes_ai`
- Keeps the legacy `/api/ai/kubernetes/analyze` route gate intact.
- Do not present it as a primary Pulse Pro pillar on current v6 surfaces.
### Legacy / Retired Claims
These should not appear as current v6 Pro promises unless they are rebuilt
into first-class product surfaces:
- `incident memory` as a standalone feature name
- `scheduled remediations`
- `execution audit trail`
## Feature Matrix
Legend:
@@ -58,7 +58,7 @@ The following should not be treated as primary marketed Pro pillars:
capability bundle and confirms that Community / Relay / Pro monitored-system
limits are all uncapped.
2. `pulse-pro/license-server/public_pricing.go` already markets Pro as
root-cause analysis, safe remediation, longer incident memory, and included
root-cause analysis, safe remediation, 90-day history, and included
team extras.
3. `frontend-modern/src/components/Alerts/InvestigateAlertButton.tsx` and
`frontend-modern/src/features/patrol/usePatrolIntelligenceState.ts` show
+14 -15
View File
@@ -4334,21 +4334,7 @@
]
}
],
"work_claims": [
{
"id": "codex-lane-l1",
"agent_id": "codex",
"summary": "Guard forward release signing against accidental trust-root drift",
"target_id": "v6-product-lane-expansion",
"claimed_at": "2026-04-22T18:47:37Z",
"heartbeat_at": "2026-04-22T18:47:37Z",
"expires_at": "2026-04-22T20:47:37Z",
"work_item": {
"kind": "lane",
"id": "L1"
}
}
],
"work_claims": [],
"open_decisions": [],
"source_of_truth_file": "docs/release-control/v6/internal/SOURCE_OF_TRUTH.md",
"resolved_decisions": [
@@ -4853,6 +4839,19 @@
"L8",
"L9"
]
},
{
"id": "self-hosted-paid-surface-classification",
"summary": "Current v6 self-hosted paid surfaces now classify commercial capabilities explicitly: only the primary Pro pillars (root-cause analysis, safe remediation, 90-day history, and included admin extras) may be marketed in customer-facing copy and upgrade prompts; compatibility-only gates such as `kubernetes_ai` remain valid runtime facts but non-marketed, and legacy claims such as `incident memory`, `scheduled remediations`, and `execution audit trail` stay retired unless rebuilt into first-class product surfaces.",
"kind": "pricing",
"decided_at": "2026-04-23",
"subsystem_ids": [
"cloud-paid"
],
"lane_ids": [
"L3",
"L9"
]
}
]
}
@@ -261,7 +261,7 @@ Community limit enforcement.
pricing contract, and owned upgrade reasons. Customer-facing self-hosted
copy must keep the ladder explicit as `Community = monitor`, `Relay = reach
Pulse from anywhere`, and `Pro = investigate root cause, apply safe
remediation, and retain longer incident memory`. Team/admin extras such as
remediation, and retain 90-day history`. Team/admin extras such as
RBAC, SSO, audit logging, reporting, and agent profiles may remain present,
but they are secondary included value and must not displace that operator
outcome framing on owned commercial surfaces.
@@ -294,7 +294,10 @@ Community limit enforcement.
SSO, RBAC, audit logging, reporting, and agent profiles may appear as
secondary included extras, while platform-specific compatibility keys such
as `kubernetes_ai` must not be elevated into a marquee marketed Pro line
item on the self-hosted Plans surface.
item on the self-hosted Plans surface. Legacy packaging nouns such as
`incident memory`, `scheduled remediations`, and `execution audit trail`
must likewise stay out of current v6 commercial copy unless Pulse ships a
first-class product surface that makes those names truthful again.
26. Keep hosted trial-activation verifier source selection compile-time owned.
`pkg/licensing/trial_activation.go`,
`pkg/licensing/trial_activation_public_key_override_dev.go`, and
@@ -1334,7 +1337,8 @@ the no-cap monitored-system model as well. `ProLicensePanel.tsx`,
self-hosted packages as unlimited core monitoring plus plan-specific extras:
Community stays free for core monitoring, Relay adds remote access/mobile/push
convenience and 14-day history, Pro adds Relay plus AI operations, automation,
advanced administration, and 90-day history, while Pro+ remains legacy
root-cause analysis, safe remediation, advanced administration, and 90-day
history, while Pro+ remains legacy
continuity only. Cloud/MSP pricing semantics stay separate, and grandfathered
v5 continuity copy remains an explicit boundary policy.
That same settings-owned presentation must distinguish between active
@@ -1383,7 +1387,10 @@ That same `frontend-modern/src/utils/licensePresentation.ts` owner must treat
compatibility-only capability keys such as `kubernetes_ai` as non-marketed
technical facts: API/docs surfaces may still name the compatibility route, but
customer-facing self-hosted current-plan summaries and unlocked-capability
lists must not surface those keys as marquee Pro value.
lists must not surface those keys as marquee Pro value. The same rule applies
to legacy claims such as `incident memory`: current v6 upgrade notices and
commercial copy must use the canonical 90-day history framing until a distinct
incident-memory product exists.
That same plan-section boundary must also defer notice resolution to component
runtime. `frontend-modern/src/components/Settings/ProLicensePlanSection.tsx`
may not compute trial-ended notices at module scope, because hosted settings
@@ -728,7 +728,7 @@ describe('ProLicensePanel', () => {
expect(screen.getByText('Push Notifications')).toBeInTheDocument();
expect(screen.getByText('Extended Metric History')).toBeInTheDocument();
expect(screen.getByText('Multi-Tenant Mode')).toBeInTheDocument();
expect(screen.queryByText('Kubernetes Analysis')).not.toBeInTheDocument();
expect(screen.queryByText('Kubernetes AI Analysis (Compatibility)')).not.toBeInTheDocument();
expect(screen.queryByText('Multi-User Mode')).not.toBeInTheDocument();
expect(screen.queryByText('White-Label Branding')).not.toBeInTheDocument();
expect(screen.queryByText('Unlimited Instances')).not.toBeInTheDocument();
@@ -1155,7 +1155,7 @@ describe('ProLicensePanel', () => {
);
expect(proLicensePlanSectionSource).not.toContain('Your Pro trial has ended');
expect(proLicensePlanSectionSource).not.toContain(
'Turn alert noise into root-cause answers, safe fixes, and 90-day incident history.',
'Turn alert noise into root-cause answers, safe fixes, and 90-day history.',
);
expect(selfHostedCommercialRecoverySectionSource).toContain(
'SELF_HOSTED_RECOVERY_PRESENTATION',
@@ -63,7 +63,7 @@ export const SELF_HOSTED_PRO_BILLING_PRESENTATION: SelfHostedProBillingPresentat
'Pulse waits for the session presentation policy before showing license, billing, or usage details.',
planSelectionPromptTitle: 'Compare self-hosted plans',
planSelectionPromptBody:
'Community includes self-hosted monitoring. Look at Relay for secure access from anywhere, or Pulse Pro for root-cause answers, safe remediation, and 90-day incident history.',
'Community includes self-hosted monitoring. Look at Relay for secure access from anywhere, or Pulse Pro for root-cause answers, safe remediation, and 90-day history.',
planSelectionPromptActionLabel: 'Compare plans',
purchaseActivatedPlanActionLabel: 'Review plan',
purchaseCancelledActionLabel: 'Compare plans',
@@ -72,7 +72,7 @@ export const SELF_HOSTED_PRO_BILLING_PRESENTATION: SelfHostedProBillingPresentat
purchaseUnavailableActionLabel: 'Try again',
trialStartTitle: 'Try Pro for free',
trialStartBody:
'Start a 14-day Pro trial for root-cause analysis, safe auto-fix, and 90-day incident history on this organization.',
'Start a 14-day Pro trial for root-cause analysis, safe auto-fix, and 90-day history on this organization.',
trialStartIdleActionLabel: 'Start 14-day Pro Trial',
trialStartPendingActionLabel: 'Starting...',
recoverySectionTitle: 'Activation & Recovery',
@@ -51,13 +51,13 @@ describe('licensePresentation', () => {
expect(getTrialEndedProLicenseNotice()).toEqual({
tone: 'border-red-200 dark:border-red-900 bg-red-50 dark:bg-red-900 text-red-900 dark:text-red-100',
title: 'Your Pro trial has ended',
body: 'Upgrade to keep root-cause analysis, safe auto-fix, and 90-day incident history.',
body: 'Upgrade to keep root-cause analysis, safe auto-fix, and 90-day history.',
actionLabel: 'View Pro plans',
});
expect(getInactiveProUpsellNotice()).toEqual({
tone: 'border-amber-200 dark:border-amber-800 bg-amber-50 dark:bg-amber-900 text-amber-800 dark:text-amber-200',
title: 'Upgrade to Pro',
body: 'Turn alert noise into root-cause answers, safe fixes, and 90-day incident history.',
body: 'Turn alert noise into root-cause answers, safe fixes, and 90-day history.',
actionLabel: 'View Pro plans',
});
expect(SELF_HOSTED_RECOVERY_PRESENTATION).toMatchObject({
@@ -97,7 +97,7 @@ describe('licensePresentation', () => {
'Pulse waits for the session presentation policy before showing license, billing, or usage details.',
planSelectionPromptTitle: 'Compare self-hosted plans',
planSelectionPromptBody:
'Community includes self-hosted monitoring. Look at Relay for secure access from anywhere, or Pulse Pro for root-cause answers, safe remediation, and 90-day incident history.',
'Community includes self-hosted monitoring. Look at Relay for secure access from anywhere, or Pulse Pro for root-cause answers, safe remediation, and 90-day history.',
planSelectionPromptActionLabel: 'Compare plans',
purchaseActivatedPlanActionLabel: 'Review plan',
purchaseCancelledActionLabel: 'Compare plans',
@@ -106,7 +106,7 @@ describe('licensePresentation', () => {
purchaseUnavailableActionLabel: 'Try again',
trialStartTitle: 'Try Pro for free',
trialStartBody:
'Start a 14-day Pro trial for root-cause analysis, safe auto-fix, and 90-day incident history on this organization.',
'Start a 14-day Pro trial for root-cause analysis, safe auto-fix, and 90-day history on this organization.',
trialStartIdleActionLabel: 'Start 14-day Pro Trial',
trialStartPendingActionLabel: 'Starting...',
recoverySectionTitle: 'Activation & Recovery',
@@ -37,7 +37,7 @@ const FEATURE_LABELS: Record<string, string> = {
ai_patrol: 'Pulse Patrol',
ai_alerts: 'Pulse Alert Analysis',
ai_autofix: 'Patrol Auto-Fix',
kubernetes_ai: 'Kubernetes Analysis',
kubernetes_ai: 'Kubernetes AI Analysis (Compatibility)',
update_alerts: 'Update Alerts',
sso: 'Basic SSO (OIDC)',
advanced_sso: 'Advanced SSO (SAML/Multi-Provider)',
@@ -761,14 +761,14 @@ export const getNoActiveProLicenseState = (): LicenseLoadingStateCopy => ({
export const getTrialEndedProLicenseNotice = (): LicenseActionNotice => ({
tone: 'border-red-200 dark:border-red-900 bg-red-50 dark:bg-red-900 text-red-900 dark:text-red-100',
title: 'Your Pro trial has ended',
body: 'Upgrade to keep root-cause analysis, safe auto-fix, and 90-day incident history.',
body: 'Upgrade to keep root-cause analysis, safe auto-fix, and 90-day history.',
actionLabel: 'View Pro plans',
});
export const getInactiveProUpsellNotice = (): LicenseActionNotice => ({
tone: 'border-amber-200 dark:border-amber-800 bg-amber-50 dark:bg-amber-900 text-amber-800 dark:text-amber-200',
title: 'Upgrade to Pro',
body: 'Turn alert noise into root-cause answers, safe fixes, and 90-day incident history.',
body: 'Turn alert noise into root-cause answers, safe fixes, and 90-day history.',
actionLabel: 'View Pro plans',
});
-1
View File
@@ -5591,7 +5591,6 @@ func TestContract_SelfHostedCommunityEntitlementsJSONSnapshot(t *testing.T) {
{"key":"long_term_metrics","reason":"Get Relay for 14 days of history, or Pro for 90 days, so you can see what changed before and after an incident.","action_url":"https://pulserelay.pro/pricing?utm_source=pulse\u0026utm_medium=app\u0026utm_campaign=upgrade\u0026feature=long_term_metrics"},
{"key":"ai_autofix","reason":"Upgrade to Pro so Pulse can move from finding issues to applying safe remediation with your approval or in autonomous mode.","action_url":"https://pulserelay.pro/pricing?utm_source=pulse\u0026utm_medium=app\u0026utm_campaign=upgrade\u0026feature=ai_autofix"},
{"key":"ai_alerts","reason":"Upgrade to Pro so alerts arrive with root-cause analysis instead of a stack of symptoms.","action_url":"https://pulserelay.pro/pricing?utm_source=pulse\u0026utm_medium=app\u0026utm_campaign=upgrade\u0026feature=ai_alerts"},
{"key":"kubernetes_ai","reason":"Upgrade to Pro so Pulse can explain cluster pressure, failing pods, and likely causes without manual Kubernetes triage.","action_url":"https://pulserelay.pro/pricing?utm_source=pulse\u0026utm_medium=app\u0026utm_campaign=upgrade\u0026feature=kubernetes_ai"},
{"key":"rbac","reason":"Upgrade to Pro when more than one operator needs safe access boundaries around infrastructure changes.","action_url":"https://pulserelay.pro/pricing?utm_source=pulse\u0026utm_medium=app\u0026utm_campaign=upgrade\u0026feature=rbac"},
{"key":"agent_profiles","reason":"Upgrade to Pro to standardize agent behavior across systems without reconfiguring every install by hand.","action_url":"https://pulserelay.pro/pricing?utm_source=pulse\u0026utm_medium=app\u0026utm_campaign=upgrade\u0026feature=agent_profiles"},
{"key":"advanced_sso","reason":"Upgrade to Pro to connect your identity provider and keep operator access aligned with your existing auth controls.","action_url":"https://pulserelay.pro/pricing?utm_source=pulse\u0026utm_medium=app\u0026utm_campaign=upgrade\u0026feature=advanced_sso"},
+4 -4
View File
@@ -482,9 +482,9 @@ function routeCommercialAPI(request, response, url, scenario) {
if (request.method === 'GET' && route === '/v1/public/pricing-model') {
sendJSON(response, 200, {
title: 'Simple self-hosted pricing for Pulse',
description: 'Community keeps monitoring free. Relay lets you reach Pulse securely from anywhere. Pro explains what broke, helps fix it safely, and keeps 90 days of incident history.',
description: 'Community keeps monitoring free. Relay lets you reach Pulse securely from anywhere. Pro explains what broke, helps fix it safely, and keeps 90 days of history.',
explainer:
'Community keeps monitoring free. Relay gets Pulse securely from anywhere. Pro adds root-cause analysis, safe remediation, and 90-day incident history.',
'Community keeps monitoring free. Relay gets Pulse securely from anywhere. Pro adds root-cause analysis, safe remediation, and 90-day history.',
plans: [
{
badge: 'Recommended',
@@ -510,13 +510,13 @@ function routeCommercialAPI(request, response, url, scenario) {
title: 'Pro',
price: '$8.99',
period: '$79/year available too',
blurb: 'The operator tier for root-cause answers, safe remediation, and longer incident memory.',
blurb: 'The operator tier for root-cause answers, safe remediation, and 90-day history.',
features: [
{ tone: 'check', html: 'Unlimited <strong>self-hosted monitoring</strong>' },
{ tone: 'check', html: 'Everything in Relay' },
{ tone: 'check', html: 'Alert-triggered root-cause analysis' },
{ tone: 'check', html: 'Patrol auto-fix with approval or autonomous mode' },
{ tone: 'check', html: '90-day incident history' },
{ tone: 'check', html: '90-day history' },
],
buttons: [
{ kind: 'checkout', className: 'btn btn-secondary', tier: 'pro', planKey: 'price_pro_monthly', billingCycle: 'monthly', label: 'Buy Monthly' },
@@ -240,14 +240,14 @@ describe('services view', function() {
title: 'Pricing',
description: 'Canonical pricing model',
explainer:
'Community keeps monitoring free. Relay gets Pulse securely from anywhere. Pro adds root-cause analysis, safe remediation, and 90-day incident history.',
'Community keeps monitoring free. Relay gets Pulse securely from anywhere. Pro adds root-cause analysis, safe remediation, and 90-day history.',
plans: [
{
tierKicker: 'Pro',
title: 'Pro',
price: '$8.99',
period: '$79/year available too',
blurb: 'The operator tier for root-cause answers, safe remediation, and longer incident memory.',
blurb: 'The operator tier for root-cause answers, safe remediation, and 90-day history.',
features: [{ tone: 'check', html: 'Unlimited <strong>self-hosted monitoring</strong>' }],
buttons: [
{
@@ -273,7 +273,7 @@ describe('services view', function() {
'Pulse Account keeps checkout tied to the Pulse instance that opened it, so completed Relay or Pro purchases return to the right Plans page automatically.',
);
expect(document.getElementById('upgrade-billing-root')?.innerHTML).toContain(
'Community keeps monitoring free. Relay gets Pulse securely from anywhere. Pro adds root-cause analysis, safe remediation, and 90-day incident history.',
'Community keeps monitoring free. Relay gets Pulse securely from anywhere. Pro adds root-cause analysis, safe remediation, and 90-day history.',
);
expect(document.getElementById('upgrade-billing-root')?.innerHTML).not.toContain('Continue to Plans');
expect(document.getElementById('upgrade-billing-root')?.innerHTML).not.toContain('ppk_live_preview');
@@ -118,5 +118,10 @@ func expectedProOnlyFeatureSet() map[string]struct{} {
for _, feature := range license.TierFeatures[license.TierFree] {
delete(proFeatures, feature)
}
for feature := range proFeatures {
if license.IsCompatibilityOnlyFeature(feature) {
delete(proFeatures, feature)
}
}
return proFeatures
}
+1 -1
View File
@@ -80,7 +80,7 @@ func TestGetFeatureDisplayNameCoversKnownFeaturesAndFallback(t *testing.T) {
{feature: FeatureAIPatrol, want: "Pulse Patrol (Background Health Checks)"},
{feature: FeatureAIAlerts, want: "Alert Analysis"},
{feature: FeatureAIAutoFix, want: "Pulse Patrol Auto-Fix"},
{feature: FeatureKubernetesAI, want: "Kubernetes Analysis"},
{feature: FeatureKubernetesAI, want: "Kubernetes AI Analysis (Compatibility)"},
{feature: FeatureUpdateAlerts, want: "Update Alerts (Container/Package Updates)"},
{feature: FeatureRBAC, want: "Role-Based Access Control (RBAC)"},
{feature: FeatureMultiUser, want: "Multi-User Mode"},
+7
View File
@@ -76,3 +76,10 @@ func GetTierDisplayName(tier Tier) string {
func GetFeatureDisplayName(feature string) string {
return licensing.GetFeatureDisplayName(feature)
}
// IsCompatibilityOnlyFeature reports capability keys that remain valid runtime
// contracts for backwards compatibility but should not be marketed as current
// v6 commercial pillars or generic upgrade prompts.
func IsCompatibilityOnlyFeature(feature string) bool {
return licensing.IsCompatibilityOnlyFeature(feature)
}
+14 -2
View File
@@ -27,7 +27,7 @@ const (
// Pro tier features (everything in Relay, plus:)
FeatureAIAlerts = "ai_alerts" // AI analysis when alerts fire
FeatureAIAutoFix = "ai_autofix" // Automatic remediation (one-click apply)
FeatureKubernetesAI = "kubernetes_ai" // AI analysis of K8s (NOT basic monitoring)
FeatureKubernetesAI = "kubernetes_ai" // Legacy Kubernetes analysis compatibility gate (NOT basic monitoring)
FeatureAgentProfiles = "agent_profiles" // Centralized agent configuration profiles
FeatureRBAC = "rbac" // Role-Based Access Control
FeatureAuditLogging = "audit_logging" // Persistent audit logs with signing
@@ -466,7 +466,7 @@ func GetFeatureDisplayName(feature string) string {
case FeatureAIAutoFix:
return "Pulse Patrol Auto-Fix"
case FeatureKubernetesAI:
return "Kubernetes Analysis"
return "Kubernetes AI Analysis (Compatibility)"
case FeatureUpdateAlerts:
return "Update Alerts (Container/Package Updates)"
case FeatureRBAC:
@@ -503,3 +503,15 @@ func GetFeatureDisplayName(feature string) string {
return feature
}
}
var compatibilityOnlyFeatures = map[string]struct{}{
FeatureKubernetesAI: {},
}
// IsCompatibilityOnlyFeature reports capability keys that remain valid runtime
// contracts for backwards compatibility but should not be marketed as current
// v6 commercial pillars or generic upgrade prompts.
func IsCompatibilityOnlyFeature(feature string) bool {
_, ok := compatibilityOnlyFeatures[feature]
return ok
}
+10 -1
View File
@@ -225,7 +225,7 @@ func TestGetFeatureDisplayName(t *testing.T) {
{FeatureAIPatrol, "Pulse Patrol (Background Health Checks)"},
{FeatureAIAlerts, "Alert Analysis"},
{FeatureAIAutoFix, "Pulse Patrol Auto-Fix"},
{FeatureKubernetesAI, "Kubernetes Analysis"},
{FeatureKubernetesAI, "Kubernetes AI Analysis (Compatibility)"},
{FeatureUpdateAlerts, "Update Alerts (Container/Package Updates)"},
{FeatureRBAC, "Role-Based Access Control (RBAC)"},
{FeatureMultiUser, "Multi-User Mode"},
@@ -255,6 +255,15 @@ func TestGetFeatureDisplayName(t *testing.T) {
}
}
func TestIsCompatibilityOnlyFeature(t *testing.T) {
if !IsCompatibilityOnlyFeature(FeatureKubernetesAI) {
t.Fatalf("expected %q to remain compatibility-only", FeatureKubernetesAI)
}
if IsCompatibilityOnlyFeature(FeatureAIAlerts) {
t.Fatalf("did not expect %q to be compatibility-only", FeatureAIAlerts)
}
}
func TestTierMonitoredSystemLimits(t *testing.T) {
tests := []struct {
tier Tier
+7 -11
View File
@@ -12,6 +12,8 @@ type ReasonEntry struct {
// UpgradeReasonMatrix is the canonical feature-to-upgrade-reason mapping.
// Relay features use "Upgrade to Relay" messaging; Pro features use "Upgrade to Pro".
// Compatibility-only capabilities must not appear here as generic marketed
// upgrade reasons.
var UpgradeReasonMatrix = []ReasonEntry{
{
Feature: FeatureRelay,
@@ -49,41 +51,35 @@ var UpgradeReasonMatrix = []ReasonEntry{
ActionURL: UpgradeURLForFeature(FeatureAIAlerts),
Priority: 4,
},
{
Feature: FeatureKubernetesAI,
Reason: "Upgrade to Pro so Pulse can explain cluster pressure, failing pods, and likely causes without manual Kubernetes triage.",
ActionURL: UpgradeURLForFeature(FeatureKubernetesAI),
Priority: 5,
},
{
Feature: FeatureRBAC,
Reason: "Upgrade to Pro when more than one operator needs safe access boundaries around infrastructure changes.",
ActionURL: UpgradeURLForFeature(FeatureRBAC),
Priority: 6,
Priority: 5,
},
{
Feature: FeatureAgentProfiles,
Reason: "Upgrade to Pro to standardize agent behavior across systems without reconfiguring every install by hand.",
ActionURL: UpgradeURLForFeature(FeatureAgentProfiles),
Priority: 7,
Priority: 6,
},
{
Feature: FeatureAdvancedSSO,
Reason: "Upgrade to Pro to connect your identity provider and keep operator access aligned with your existing auth controls.",
ActionURL: UpgradeURLForFeature(FeatureAdvancedSSO),
Priority: 8,
Priority: 7,
},
{
Feature: FeatureAuditLogging,
Reason: "Upgrade to Pro to keep a trustworthy action trail for incident review, accountability, and compliance.",
ActionURL: UpgradeURLForFeature(FeatureAuditLogging),
Priority: 9,
Priority: 8,
},
{
Feature: FeatureAdvancedReporting,
Reason: "Upgrade to Pro to turn live infrastructure state into shareable reports without manual screenshot work.",
ActionURL: UpgradeURLForFeature(FeatureAdvancedReporting),
Priority: 10,
Priority: 9,
},
}
+9
View File
@@ -100,3 +100,12 @@ func TestOperatorOutcomeUpgradeReasonsRemainCanonical(t *testing.T) {
}
}
}
func TestCompatibilityOnlyFeaturesStayOutOfGenericUpgradeReasons(t *testing.T) {
reasons := GenerateUpgradeReasons(nil)
for _, entry := range reasons {
if entry.Feature == FeatureKubernetesAI {
t.Fatalf("compatibility-only feature %q should not produce a generic upgrade reason", entry.Feature)
}
}
}
@@ -4557,6 +4557,7 @@ class SubsystemLookupTest(unittest.TestCase):
"cloud-msp-stripe-prices",
"legacy-grandfathering-eligibility-cutoff",
"self-hosted-plans-surface-entitlement-first",
"self-hosted-paid-surface-classification",
"self-hosted-core-monitoring-free",
"self-hosted-paid-extras-packaging",
"stable-release-promotion-model",
@@ -421,7 +421,7 @@ async function openMonitoredSystemUpgradeArrival(page: Page) {
);
await expect(
page.getByText(
"Community includes self-hosted monitoring. Look at Relay for secure access from anywhere, or Pulse Pro for root-cause answers, safe remediation, and 90-day incident history.",
"Community includes self-hosted monitoring. Look at Relay for secure access from anywhere, or Pulse Pro for root-cause answers, safe remediation, and 90-day history.",
),
).toBeVisible();
await expect(page.getByRole("button", { name: "Hide counting rules" })).toHaveCount(0);
@@ -80,7 +80,7 @@ test.describe("Pulse Account upgrade bootstrap", () => {
).toBeVisible();
await expect(
page.getByText(
"Community keeps monitoring free. Relay gets Pulse securely from anywhere. Pro adds root-cause analysis, safe remediation, and 90-day incident history.",
"Community keeps monitoring free. Relay gets Pulse securely from anywhere. Pro adds root-cause analysis, safe remediation, and 90-day history.",
),
).toBeVisible();
await expect(