diff --git a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md index 9be3207f5..6908bfc06 100644 --- a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md +++ b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md @@ -558,6 +558,11 @@ profile and assignment columns, but embedded table framing must route through first-session states must keep `Add infrastructure` visible for both API-backed and agent-managed next systems instead of reviving separate `Platform connections` and `Infrastructure Install` branches. The + completion panel, infrastructure installer, install state, and agent-profile + settings surfaces must also stay free of local browser commercial or + onboarding metrics wrappers. Lifecycle surfaces may navigate to canonical + destinations, but Pulse Account and server-owned reporting routes own + commercial event capture. API-backed versus agent-managed classification must come from the governed onboarding paths in `docs/release-control/v6/internal/PLATFORM_SUPPORT_MANIFEST.json` through diff --git a/docs/release-control/v6/internal/subsystems/ai-runtime.md b/docs/release-control/v6/internal/subsystems/ai-runtime.md index 21be57c4e..484013748 100644 --- a/docs/release-control/v6/internal/subsystems/ai-runtime.md +++ b/docs/release-control/v6/internal/subsystems/ai-runtime.md @@ -143,6 +143,11 @@ runtime cost control, and shared AI transport surfaces. but frontend comments, API denial messages, runtime logs, status labels, CLI help, and commercial prompts that describe the capability must use safe remediation or remediation wording. +12. Keep AI control-level presentation runtime-owned rather than tier-owned. + `frontend-modern/src/utils/aiControlLevelPresentation.ts` and + `frontend-modern/src/components/Settings/AIRuntimeControlsSection.tsx` + may describe approval posture, but must not add Pro-badge suffixes or + local commercial tracking around those runtime controls. ## Current State diff --git a/docs/release-control/v6/internal/subsystems/alerts.md b/docs/release-control/v6/internal/subsystems/alerts.md index c2a19a7e1..3e2029f59 100644 --- a/docs/release-control/v6/internal/subsystems/alerts.md +++ b/docs/release-control/v6/internal/subsystems/alerts.md @@ -57,6 +57,11 @@ operator-facing alert routing behavior for live runtime alerts. 2. Add typed collector/builders in `internal/alerts/alerts.go` 3. Add identity/persistence updates through canonical alert helpers only 4. Add or change alert history persistence through `internal/alerts/history.go` using normalized owned storage roots and fixed storage leaves only +5. Add or change locked alert-investigation commercial handoff behavior through + `frontend-modern/src/components/Alerts/InvestigateAlertButton.tsx` while + preserving the shared upgrade-navigation contract; the alert surface may + route to the canonical destination, but must not emit browser-local upgrade + metrics or present Pro-required copy when prompt suppression applies. ## Forbidden Paths diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md index d2808500d..3da512a8a 100644 --- a/docs/release-control/v6/internal/subsystems/api-contracts.md +++ b/docs/release-control/v6/internal/subsystems/api-contracts.md @@ -116,6 +116,13 @@ admin-only local commercial reporting routes. `frontend-modern/src/utils/sourcePlatforms.ts` rather than page-local wording, because those install choices set API-token and install-command expectations for the operator. + That same browser/API boundary must not retain customer-side commercial or + onboarding telemetry wrappers around infrastructure operations. Pulse + Account and the server-owned commercial reporting routes own commercial + event ingestion; the infrastructure operations model and install state may + navigate to canonical destinations, but must not import or call local + `upgradeMetrics`, `conversionEvents`, or infrastructure onboarding metrics + wrappers. 13. `frontend-modern/src/components/Settings/NodeModalAuthenticationSection.tsx` shared with `agent-lifecycle`: the node setup authentication section is both an agent lifecycle control surface and a shared API-backed install/setup contract boundary. 16. `frontend-modern/src/components/Settings/NodeModalBasicInfoSection.tsx` shared with `agent-lifecycle`: the node setup basic-info section is both an agent lifecycle control surface and a shared API-backed install/setup contract boundary. 17. `frontend-modern/src/components/Settings/nodeModalModel.ts` shared with `agent-lifecycle`: the pure node setup modal model is both an agent lifecycle control surface and a shared API-backed install/setup contract boundary. @@ -1818,7 +1825,9 @@ rather than becoming general authenticated product reads. Authenticated event ingestion at `/api/upgrade-metrics/events` may remain only as compatibility or admin-owned ingestion plumbing; customer frontend surfaces must not call it for pricing, checkout, paywall, commercial funnel, or infrastructure-onboarding -signals. Reporting and control stay on the privileged settings boundary. +signals, and must not keep customer-side commercial/onboarding metrics wrapper +modules as a compatibility layer. Reporting and control stay on the privileged +settings boundary. That same public-demo API boundary must also hide runtime-admin operations surfaces instead of treating them as harmless reads. Demo sessions must receive `404` for `/api/diagnostics`, `/api/diagnostics/docker/prepare-token`, and the diff --git a/docs/release-control/v6/internal/subsystems/cloud-paid.md b/docs/release-control/v6/internal/subsystems/cloud-paid.md index db52b44de..2d9488e41 100644 --- a/docs/release-control/v6/internal/subsystems/cloud-paid.md +++ b/docs/release-control/v6/internal/subsystems/cloud-paid.md @@ -528,11 +528,12 @@ runtime gating as separate unlinked claims. 14. Keep self-hosted commercial funnel stage ownership out of the customer frontend. `pkg/licensing/conversion_events.go` and `pkg/licensing/conversion_store.go` may retain server/admin compatibility - handling for local commercial reporting, while - `frontend-modern/src/utils/upgradeMetrics.ts` is a compatibility no-op and - must not POST in-app `Plans & Billing`, pricing, checkout, paywall, or - onboarding events from browser product surfaces. `pulse-pro:license-server/v6_checkout.go` - owns the Pulse Account handoff equivalents bound to `portal_handoff_id`. + handling for local commercial reporting, while the customer frontend must + not contain `upgradeMetrics`, `conversionEvents`, or infrastructure + onboarding metrics wrappers or call sites at all. In-app `Plans & Billing`, + pricing, checkout, paywall, or onboarding surfaces must not emit browser + product events. `pulse-pro:license-server/v6_checkout.go` owns the Pulse + Account handoff equivalents bound to `portal_handoff_id`. Pulse must not infer those portal stages from referrer state, and the commercial service must keep those self-hosted handoffs on release track `v6` even while the public site remains on `v5` before GA. @@ -1906,16 +1907,17 @@ routes rather than a package-wide `pkg/licensing/` fallback. That same conversion-telemetry boundary now keeps self-hosted commercial progression out of customer-side browser analytics. `pkg/licensing/conversion_events.go` and `pkg/licensing/conversion_store.go` -may own local server/admin reporting compatibility, while -`frontend-modern/src/utils/upgradeMetrics.ts` remains a compatibility no-op -and must not emit local `pricing_viewed`, `checkout_clicked`, paywall, -commercial funnel, or onboarding events from the in-app `Plans & Billing` plan -surface. `pulse-pro:license-server/v6_checkout.go` owns the Pulse Account -handoff equivalents bound to `portal_handoff_id` and the canonical checkout -intent. The browser app must not try to recreate those Pulse Account stages -from referrer state, and the commercial service must not collapse self-hosted -v6 handoffs back onto the public-site release track when production public GA -is still on v5. +may own local server/admin reporting compatibility, while customer frontend +source must not retain `upgradeMetrics`, `conversionEvents`, infrastructure +onboarding metrics wrappers, or their call sites. The browser app must not emit +local `pricing_viewed`, `checkout_clicked`, paywall, commercial funnel, or +onboarding events from the in-app `Plans & Billing` plan surface. +`pulse-pro:license-server/v6_checkout.go` owns the Pulse Account handoff +equivalents bound to `portal_handoff_id` and the canonical checkout intent. +The browser app must not try to recreate those Pulse Account stages from +referrer state, and the commercial service must not collapse self-hosted v6 +handoffs back onto the public-site release track when production public GA is +still on v5. That same local conversion store remains the canonical read model for self-hosted commercial reporting, but not for user support diagnostics: admin-owned reporting surfaces may read structured local funnel reports from diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 4a328af50..fd0fd2b42 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -727,6 +727,11 @@ work extends shared components instead of creating new local variants. `frontend-modern/src/components/Settings/selfHostedBillingPresentation.ts` instead of importing generic commercial presentation helpers directly into hosted settings route shells. + Contextual settings feature gates must use capability-owned presentation + helpers and neutral paid-plan copy. They must not reintroduce `Pro feature` + badge titles, Pro-suffixed option labels, monitored-system limit claims, or + browser-local commercial/onboarding metrics wrappers in SSO, audit, + reporting, AI controls, agent profiles, or shared warning banners. 8. Keep first-session dashboard empty-state copy on `frontend-modern/src/utils/workloadEmptyStatePresentation.ts`, and make infrastructure setup guidance name the canonical destination explicitly @@ -1018,7 +1023,9 @@ telemetry`; maintainer commercial-event controls, upgrade-metrics labels, and sales/onboarding reporting language must not appear in customer-facing Settings or support diagnostics, and public configuration docs must not list their internal compatibility switches as ordinary operator settings. Customer -frontend code must also not POST those events to `/api/upgrade-metrics/events`. +frontend code must also not import, define, or call `upgradeMetrics`, +`conversionEvents`, infrastructure onboarding metrics wrappers, or POST those +events to `/api/upgrade-metrics/events`. The telemetry copy must describe normalized release identity rather than falling back to ambiguous `telemetry`, `upgrade metrics`, or raw-version wording. @@ -1784,7 +1791,10 @@ belong in admin-owned metrics surfaces instead of Settings support UI. `frontend-modern/scripts/settings-diagnostics-boundary-audit.mjs`, called by the canonical frontend audit runner, enforces that boundary by failing if the diagnostics API, diagnostics results panel, or diagnostics payload model -reintroduce those analytics fields outside the defensive strip helper. +reintroduce those analytics fields outside the defensive strip helper, and by +failing if production customer frontend source reintroduces the retired +commercial/onboarding analytics wrappers or `/api/upgrade-metrics/events` +calls. Diagnostics cards that summarize Docker and Podman agent coverage must use the shared `docker` source-platform label from `frontend-modern/src/utils/sourcePlatforms.ts` for their heading and body copy, diff --git a/docs/release-control/v6/internal/subsystems/organization-settings.md b/docs/release-control/v6/internal/subsystems/organization-settings.md index 727750091..b38602750 100644 --- a/docs/release-control/v6/internal/subsystems/organization-settings.md +++ b/docs/release-control/v6/internal/subsystems/organization-settings.md @@ -92,6 +92,10 @@ create, review, and approve cross-organization shares. 2. Keep organization settings copy and validation inside the canonical organization presentation helpers 3. Keep the shared organization and RBAC transport proof routes explicit in `registry.json`; default fallback proof routing is not allowed for this subsystem 4. Update this contract whenever a new organization settings, role-management, or organization-domain helper entry point becomes canonical runtime surface area +5. Keep RBAC feature-gate copy on the organization presentation owner. RBAC + may describe capability availability on paid self-hosted and hosted plans, + but must not use Pro-badge titles or local browser upgrade telemetry in + `RBACFeatureGateSection.tsx` or `useRBACFeatureGateState.ts`. ## Current State diff --git a/docs/release-control/v6/internal/subsystems/security-privacy.md b/docs/release-control/v6/internal/subsystems/security-privacy.md index bbddd80c8..9d3f69bd9 100644 --- a/docs/release-control/v6/internal/subsystems/security-privacy.md +++ b/docs/release-control/v6/internal/subsystems/security-privacy.md @@ -212,7 +212,9 @@ concern in `frontend-modern/src/stores/systemSettings.ts`, not settings navigation metadata or other frontend-primitives owners. Retired local-upgrade-metrics compatibility must not become customer-side commercial analytics emission; browser product surfaces must not POST pricing, -checkout, paywall, funnel, or onboarding signals to `/api/upgrade-metrics/events`. +checkout, paywall, funnel, or onboarding signals to `/api/upgrade-metrics/events`, +and customer frontend source must not keep `upgradeMetrics`, `conversionEvents`, +or infrastructure onboarding metrics wrappers as compatibility imports. The security transport surfaces remain intentionally shared with `api-contracts`: token, auth, and telemetry settings payloads are still API diff --git a/frontend-modern/scripts/__tests__/settings-diagnostics-boundary-audit.test.mjs b/frontend-modern/scripts/__tests__/settings-diagnostics-boundary-audit.test.mjs index d35dacef0..69b1a6938 100644 --- a/frontend-modern/scripts/__tests__/settings-diagnostics-boundary-audit.test.mjs +++ b/frontend-modern/scripts/__tests__/settings-diagnostics-boundary-audit.test.mjs @@ -16,8 +16,6 @@ function writeFixture(root, repoRoot, files) { 'export function DiagnosticsResultsPanel() { return null; }\n', [path.join(root, 'src', 'components', 'Settings', 'diagnosticsModel.ts')]: 'export interface DiagnosticsInfo { version: string; }\n', - [path.join(root, 'src', 'utils', 'upgradeMetrics.ts')]: - 'export function trackUpgradeMetricEvent() {}\n', ...files, }; @@ -85,25 +83,25 @@ export function stripInternalAnalyticsDiagnosticsFields(payload) { expect(collectUserDiagnosticsInternalAnalyticsFindings({ root, repoRoot })).toEqual([]); }); - it('reports product-side upgrade-metrics ingestion plumbing', () => { + it('reports production frontend commercial analytics shims', () => { const { root, repoRoot } = makeFixture(({ root }) => ({ - [path.join(root, 'src', 'utils', 'upgradeMetrics.ts')]: ` -import { apiFetch } from '@/utils/apiClient'; + [path.join(root, 'src', 'components', 'Settings', 'CommercialProbe.tsx')]: ` +import { trackPaywallViewed } from '@/utils/upgradeMetrics'; -export function trackUpgradeMetricEvent() { - void apiFetch('/api/upgrade-metrics/events'); +export function CommercialProbe() { + const onboardingMetricsTracker = { recordOpened() {} }; + trackPaywallViewed('rbac', 'settings_roles_panel'); + onboardingMetricsTracker.recordOpened(); + return null; } `, })); const findings = collectUserDiagnosticsInternalAnalyticsFindings({ root, repoRoot }); - expect(findings.map((finding) => finding.rule)).toEqual([ - 'canonical-settings/no-product-upgrade-metrics-ingestion', - 'canonical-settings/no-product-upgrade-metrics-ingestion', - 'canonical-settings/no-product-upgrade-metrics-ingestion', - 'canonical-settings/no-product-upgrade-metrics-endpoint', - ]); + expect(findings.map((finding) => finding.rule)).toEqual( + Array(5).fill('canonical-settings/no-product-commercial-analytics-source'), + ); }); it('reports direct production frontend calls to upgrade-metrics ingestion', () => { diff --git a/frontend-modern/scripts/settings-diagnostics-boundary-audit.mjs b/frontend-modern/scripts/settings-diagnostics-boundary-audit.mjs index 46649a3cf..c887c72c3 100644 --- a/frontend-modern/scripts/settings-diagnostics-boundary-audit.mjs +++ b/frontend-modern/scripts/settings-diagnostics-boundary-audit.mjs @@ -47,21 +47,16 @@ const INTERNAL_ANALYTICS_RULES = [ }, ], }, - { - getFilePath: ({ root }) => path.join(root, 'src', 'utils', 'upgradeMetrics.ts'), - rules: [ - { - rule: 'canonical-settings/no-product-upgrade-metrics-ingestion', - regex: - /\/api\/upgrade-metrics\/events|@\/utils\/apiClient|\bapiFetch\s*\(|\bfetch\s*\(|\bsendBeacon\b/g, - message: - 'Do not emit maintainer/admin commercial analytics from the customer frontend. Keep upgrade-metrics ingestion out of product surfaces.', - }, - ], - }, { getFilePaths: ({ root }) => listProductionSourceFiles(path.join(root, 'src')), rules: [ + { + rule: 'canonical-settings/no-product-commercial-analytics-source', + regex: + /@\/utils\/(?:conversionEvents|infrastructureOnboardingMetrics|upgradeMetrics)|['"]\.\.?\/(?:conversionEvents|infrastructureOnboardingMetrics|upgradeMetrics)['"]|\b(?:InfrastructureOnboardingMetrics|InfrastructureOnboardingMetricsTracker|UPGRADE_METRIC_EVENTS|UNIFIED_AGENT_TELEMETRY_SURFACE|clearSharedInfrastructureOnboardingMetricsTracker|createInfrastructureOnboardingMetricsTracker|getSharedInfrastructureOnboardingMetricsTracker|normalizeTelemetryPart|onboardingMetricsTracker|trackAgentFirstConnected|trackAgentInstallCommandCopied|trackAgentInstallProfileSelected|trackAgentInstallTokenGenerated|trackCheckoutClicked|trackPaywallViewed|trackPricingViewed|trackUpgradeClicked|trackUpgradeMetricEvent)\b/g, + message: + 'Do not keep maintainer/admin commercial or onboarding analytics shims in production customer frontend source.', + }, { rule: 'canonical-settings/no-product-upgrade-metrics-endpoint', regex: /\/api\/upgrade-metrics\/events/g, diff --git a/frontend-modern/src/components/AI/Chat/__tests__/AIChat.test.tsx b/frontend-modern/src/components/AI/Chat/__tests__/AIChat.test.tsx index ffb4887f7..8cbfb8b9f 100644 --- a/frontend-modern/src/components/AI/Chat/__tests__/AIChat.test.tsx +++ b/frontend-modern/src/components/AI/Chat/__tests__/AIChat.test.tsx @@ -446,7 +446,7 @@ describe('AIChat', () => { expect(screen.getByText('Default control mode')).toBeInTheDocument(); expect(screen.getByText('No commands or control actions')).toBeInTheDocument(); expect(screen.getByText('Ask before running commands')).toBeInTheDocument(); - expect(screen.getByText('Executes without approval (Pro)')).toBeInTheDocument(); + expect(screen.getByText('Executes without approval')).toBeInTheDocument(); }); }); diff --git a/frontend-modern/src/components/Alerts/InvestigateAlertButton.tsx b/frontend-modern/src/components/Alerts/InvestigateAlertButton.tsx index e79a422a1..20dec2dc4 100644 --- a/frontend-modern/src/components/Alerts/InvestigateAlertButton.tsx +++ b/frontend-modern/src/components/Alerts/InvestigateAlertButton.tsx @@ -7,7 +7,6 @@ import { formatAlertValue } from '@/utils/alertFormatters'; import { resolveAlertTargetType } from '@/utils/alertTargetTypes'; import { getUpgradeActionDestination } from '@/stores/licenseCommercial'; import { presentationPolicyHidesUpgradePrompts } from '@/stores/sessionPresentationPolicy'; -import { trackUpgradeClicked } from '@/utils/upgradeMetrics'; interface InvestigateAlertButtonProps { alert: Alert; @@ -47,7 +46,6 @@ export function InvestigateAlertButton(props: InvestigateAlertButtonProps) { if (!canShowUpgradePrompt()) { return; } - trackUpgradeClicked('investigate_alert_button', 'ai_alerts'); openUpgradeDestination(getUpgradeActionDestination('ai_alerts')); return; } diff --git a/frontend-modern/src/components/Alerts/__tests__/InvestigateAlertButton.test.tsx b/frontend-modern/src/components/Alerts/__tests__/InvestigateAlertButton.test.tsx index 904d6685c..2f5e7c7cc 100644 --- a/frontend-modern/src/components/Alerts/__tests__/InvestigateAlertButton.test.tsx +++ b/frontend-modern/src/components/Alerts/__tests__/InvestigateAlertButton.test.tsx @@ -10,7 +10,6 @@ import { getPublicPricingUrl } from '@/utils/pricingHandoff'; const { openWithPromptMock, openUpgradeDestinationMock, - trackUpgradeClickedMock, formatAlertValueMock, mockAiChatStore, getUpgradeActionDestinationMock, @@ -20,7 +19,6 @@ const { vi.hoisted(() => { const openWithPromptMock = vi.fn(); const openUpgradeDestinationMock = vi.fn(); - const trackUpgradeClickedMock = vi.fn(); const formatAlertValueMock = vi.fn((value?: number, _type?: string) => value !== undefined ? `${value.toFixed(1)}%` : 'N/A', ); @@ -34,7 +32,6 @@ const { return { openWithPromptMock, openUpgradeDestinationMock, - trackUpgradeClickedMock, formatAlertValueMock, mockAiChatStore, getUpgradeActionDestinationMock, @@ -60,10 +57,6 @@ vi.mock('@/components/shared/useUpgradeNavigation', () => ({ useUpgradeNavigation: () => openUpgradeDestinationMock, })); -vi.mock('@/utils/upgradeMetrics', () => ({ - trackUpgradeClicked: (...args: unknown[]) => trackUpgradeClickedMock(...args), -})); - vi.mock('@/utils/alertFormatters', () => ({ formatAlertValue: (...args: unknown[]) => formatAlertValueMock(...(args as [number, string])), })); @@ -105,7 +98,6 @@ afterEach(() => { beforeEach(() => { openWithPromptMock.mockReset(); - trackUpgradeClickedMock.mockReset(); formatAlertValueMock.mockClear(); openUpgradeDestinationMock.mockReset(); getUpgradeActionDestinationMock.mockReset(); @@ -231,12 +223,11 @@ describe('InvestigateAlertButton', () => { expect(button.className).toContain('opacity-60'); }); - it('opens upgrade URL and tracks click when locked and clicked', async () => { + it('opens upgrade URL when locked and clicked', async () => { render(() => ); const button = screen.getByRole('button'); await fireEvent.click(button); - expect(trackUpgradeClickedMock).toHaveBeenCalledWith('investigate_alert_button', 'ai_alerts'); expect(openUpgradeDestinationMock).toHaveBeenCalledWith({ href: getPublicPricingUrl('ai_alerts'), external: true, @@ -257,7 +248,6 @@ describe('InvestigateAlertButton', () => { await fireEvent.click(button); - expect(trackUpgradeClickedMock).not.toHaveBeenCalled(); expect(openUpgradeDestinationMock).not.toHaveBeenCalled(); expect(openWithPromptMock).not.toHaveBeenCalled(); }); @@ -314,10 +304,9 @@ describe('InvestigateAlertButton', () => { }); }); - it('does not call trackUpgradeClicked when unlocked', async () => { + it('does not open the upgrade destination when unlocked', async () => { render(() => ); await fireEvent.click(screen.getByRole('button')); - expect(trackUpgradeClickedMock).not.toHaveBeenCalled(); expect(openUpgradeDestinationMock).not.toHaveBeenCalled(); }); diff --git a/frontend-modern/src/components/Settings/AIRuntimeControlsSection.tsx b/frontend-modern/src/components/Settings/AIRuntimeControlsSection.tsx index 6801820e3..02baa9bc8 100644 --- a/frontend-modern/src/components/Settings/AIRuntimeControlsSection.tsx +++ b/frontend-modern/src/components/Settings/AIRuntimeControlsSection.tsx @@ -16,7 +16,6 @@ import { getAISettingsWorkloadDiscoveryHelpContent, getAISettingsWorkloadDiscoverySummary, } from '@/utils/aiSettingsPresentation'; -import { trackUpgradeClicked } from '@/utils/upgradeMetrics'; import { UPGRADE_ACTION_LABEL } from '@/utils/upgradePresentation'; interface AIRuntimeControlsSectionProps { @@ -256,7 +255,6 @@ export const AIRuntimeControlsSection: Component @@ -289,7 +287,6 @@ export const AIRuntimeControlsSection: Component trackUpgradeClicked('settings_ai_patrol_autofix', 'ai_autofix')} > {UPGRADE_ACTION_LABEL} {' '} diff --git a/frontend-modern/src/components/Settings/AgentProfilesPanel.tsx b/frontend-modern/src/components/Settings/AgentProfilesPanel.tsx index 5ad9f7159..c62b5b648 100644 --- a/frontend-modern/src/components/Settings/AgentProfilesPanel.tsx +++ b/frontend-modern/src/components/Settings/AgentProfilesPanel.tsx @@ -7,6 +7,7 @@ import { UpgradeLink } from '@/components/shared/UpgradeLink'; import { SuggestProfileModal } from './SuggestProfileModal'; import { KNOWN_SETTINGS, type SelectSetting, type StringSetting } from './agentProfileSettings'; import { + getAgentProfilesFeatureGateCopy, getAgentProfileAssignmentsEmptyState, getAgentProfilesEmptyState, } from '@/utils/agentProfilesPresentation'; @@ -21,6 +22,7 @@ import { PulseDataGrid } from '@/components/shared/PulseDataGrid'; import { useAgentProfilesPanelState } from './useAgentProfilesPanelState'; export const AgentProfilesPanel: Component = () => { + const featureGateCopy = getAgentProfilesFeatureGateCopy(); const { aiAvailable, checkingLicense, @@ -56,7 +58,6 @@ export const AgentProfilesPanel: Component = () => { showUpgradePrompts, showModal, showSuggestModal, - trackUpgradeClicked, unknownKeys, updateSetting, formatRelativeTime, @@ -87,21 +88,17 @@ export const AgentProfilesPanel: Component = () => {
-

Agent Profiles

-

Pro feature

+

+ {featureGateCopy.title} +

+

{featureGateCopy.subtitle}

-

- Create reusable configuration profiles for your agents. Manage settings like Docker - monitoring, logging levels, and reporting intervals from a central location. -

+

{featureGateCopy.body}

- trackUpgradeClicked('settings_agent_profiles_panel', 'agent_profiles') - } > {UPGRADE_ACTION_LABEL} diff --git a/frontend-modern/src/components/Settings/AuditLogPanel.tsx b/frontend-modern/src/components/Settings/AuditLogPanel.tsx index 8899d8dc1..d81780e54 100644 --- a/frontend-modern/src/components/Settings/AuditLogPanel.tsx +++ b/frontend-modern/src/components/Settings/AuditLogPanel.tsx @@ -50,7 +50,6 @@ export default function AuditLogPanel() { goToLastPage, goToNextPage, goToPreviousPage, - handleUpgradeClick, hasNextPage, hasResumeEvents, hasSignedEvents, @@ -170,7 +169,6 @@ export default function AuditLogPanel() { {UPGRADE_ACTION_LABEL} diff --git a/frontend-modern/src/components/Settings/AuditWebhookPanel.tsx b/frontend-modern/src/components/Settings/AuditWebhookPanel.tsx index d7a54ff7f..fe47ab2e9 100644 --- a/frontend-modern/src/components/Settings/AuditWebhookPanel.tsx +++ b/frontend-modern/src/components/Settings/AuditWebhookPanel.tsx @@ -18,7 +18,6 @@ import { getAuditWebhookFeatureGateCopy, getAuditWebhookLoadingState, } from '@/utils/auditWebhookPresentation'; -import { trackUpgradeClicked } from '@/utils/upgradeMetrics'; import { getUpgradeActionButtonClass, UPGRADE_ACTION_LABEL, @@ -65,9 +64,6 @@ export const AuditWebhookPanel: Component = (props) => { - trackUpgradeClicked('settings_audit_webhook_panel', 'audit_logging') - } > {UPGRADE_ACTION_LABEL} diff --git a/frontend-modern/src/components/Settings/ConnectionEditor/ConnectionEditor.tsx b/frontend-modern/src/components/Settings/ConnectionEditor/ConnectionEditor.tsx index a5295d196..45dd702c1 100644 --- a/frontend-modern/src/components/Settings/ConnectionEditor/ConnectionEditor.tsx +++ b/frontend-modern/src/components/Settings/ConnectionEditor/ConnectionEditor.tsx @@ -1,4 +1,4 @@ -import { Component, For, type JSX, Show, createEffect, createMemo, createSignal, onMount } from 'solid-js'; +import { Component, For, type JSX, Show, createMemo, createSignal } from 'solid-js'; import type { ConnectionType, ProbeCandidate } from '@/api/connections'; import { AddressProbeStep } from './AddressProbeStep'; import { @@ -7,10 +7,6 @@ import { type ConnectionEditorState, } from './useConnectionEditor'; import { getInfrastructureAutoDetectLabels } from '@/utils/infrastructureOnboardingPresentation'; -import { - createInfrastructureOnboardingMetricsTracker, - type InfrastructureOnboardingMetricsTracker, -} from '@/utils/infrastructureOnboardingMetrics'; export type ConnectionEditorMode = 'add' | 'edit'; @@ -30,8 +26,6 @@ export interface ConnectionEditorProps { initialAddress?: string; initialCandidate?: ProbeCandidate | null; showSlotHeader?: boolean; - trackInitialCatalogSelection?: boolean; - onboardingMetricsTracker?: InfrastructureOnboardingMetricsTracker | null; onBackToCatalog?: () => void; onSelectAgentRoute?: () => void; onSelectCandidate?: (candidate: ProbeCandidate) => void; @@ -52,42 +46,11 @@ export const ConnectionEditor: Component = (props) => { const [selectedCandidate, setSelectedCandidate] = createSignal( props.initialCandidate ?? null, ); - const ownsOnboardingMetricsTracker = - (props.mode ?? 'add') === 'add' && !props.onboardingMetricsTracker; - const onboardingMetrics = - (props.mode ?? 'add') === 'add' - ? props.onboardingMetricsTracker ?? createInfrastructureOnboardingMetricsTracker() - : null; - const activeType = () => selectedType(); const showCredentialSlot = () => activeType() !== null; const autoDetectLabels = createMemo(() => getInfrastructureAutoDetectLabels()); - const recordPathSelectedForType = (type: ConnectionType) => { - onboardingMetrics?.recordPathSelected(type === 'agent' ? 'agent' : 'api'); - }; - - onMount(() => { - if (!onboardingMetrics) return; - if (ownsOnboardingMetricsTracker) { - onboardingMetrics.recordOpened(); - } - if (props.initialType) { - recordPathSelectedForType(props.initialType); - if (props.trackInitialCatalogSelection && props.initialType !== 'agent') { - onboardingMetrics.recordCatalogSelected(props.initialType); - } - } - }); - - createEffect(() => { - const type = selectedType(); - if (!onboardingMetrics || !type) return; - onboardingMetrics.recordCredentialsOpened(type); - }); - const chooseCandidate = (candidate: ProbeCandidate) => { - onboardingMetrics?.recordPathSelected('api'); if (props.onSelectCandidate) { props.onSelectCandidate(candidate); return; @@ -97,14 +60,12 @@ export const ConnectionEditor: Component = (props) => { }; const chooseManualType = (type: ConnectionType) => { - recordPathSelectedForType(type); setSelectedCandidate(null); setSelectedType(type); }; const installAgent = () => { if (props.onSelectAgentRoute) { - onboardingMetrics?.recordPathSelected('agent'); props.onSelectAgentRoute(); return; } @@ -175,8 +136,6 @@ export const ConnectionEditor: Component = (props) => { onSelectCandidate={chooseCandidate} onInstallAgent={installAgent} onChooseSourceTypeInstead={props.onBackToCatalog} - onProbeSubmitted={() => onboardingMetrics?.recordPathSelected('api')} - onProbeResolved={(outcome) => onboardingMetrics?.recordProbeResult(outcome)} />
diff --git a/frontend-modern/src/components/Settings/ConnectionEditor/__tests__/ConnectionEditor.test.tsx b/frontend-modern/src/components/Settings/ConnectionEditor/__tests__/ConnectionEditor.test.tsx index 7efcbd017..00a145355 100644 --- a/frontend-modern/src/components/Settings/ConnectionEditor/__tests__/ConnectionEditor.test.tsx +++ b/frontend-modern/src/components/Settings/ConnectionEditor/__tests__/ConnectionEditor.test.tsx @@ -9,25 +9,11 @@ const connectionsApiMock = vi.hoisted(() => ({ setEnabled: vi.fn(), remove: vi.fn(), })); -const onboardingMetricsTrackerMock = vi.hoisted(() => ({ - recordOpened: vi.fn(), - recordPathSelected: vi.fn(), - recordProbeResult: vi.fn(), - recordCatalogSelected: vi.fn(), - recordCredentialsOpened: vi.fn(), -})); -const createInfrastructureOnboardingMetricsTrackerMock = vi.hoisted(() => - vi.fn(() => onboardingMetricsTrackerMock), -); vi.mock('@/api/connections', () => ({ ConnectionsAPI: connectionsApiMock, })); -vi.mock('@/utils/infrastructureOnboardingMetrics', () => ({ - createInfrastructureOnboardingMetricsTracker: createInfrastructureOnboardingMetricsTrackerMock, -})); - const mockedProbe = vi.mocked(ConnectionsAPI.probe); describe('ConnectionEditor', () => { @@ -47,8 +33,6 @@ describe('ConnectionEditor', () => { const renderSlot = vi.fn((props) =>
slot:{props.type}
); render(() => {}} />); - expect(createInfrastructureOnboardingMetricsTrackerMock).toHaveBeenCalledTimes(1); - expect(onboardingMetricsTrackerMock.recordOpened).toHaveBeenCalledTimes(1); const input = screen.getByPlaceholderText(/vcenter\.lab/) as HTMLInputElement; fireEvent.input(input, { target: { value: 'pve.lab' } }); @@ -56,15 +40,10 @@ describe('ConnectionEditor', () => { fireEvent.click(screen.getByRole('button', { name: /probe address/i })); await waitFor(() => expect(mockedProbe).toHaveBeenCalledWith('pve.lab')); - await waitFor(() => - expect(onboardingMetricsTrackerMock.recordProbeResult).toHaveBeenCalledWith('detected'), - ); - expect(onboardingMetricsTrackerMock.recordPathSelected).toHaveBeenCalledWith('api'); fireEvent.click((await screen.findByText('https://pve.lab:8006')).closest('button')!); await waitFor(() => expect(screen.getByTestId('slot').textContent).toBe('slot:pve')); - expect(onboardingMetricsTrackerMock.recordCredentialsOpened).toHaveBeenCalledWith('pve'); const lastCall = renderSlot.mock.calls.at(-1)![0]; expect(lastCall.type).toBe('pve'); expect(lastCall.candidate?.host).toBe('https://pve.lab:8006'); @@ -107,7 +86,9 @@ describe('ConnectionEditor', () => { fireEvent.click(screen.getByRole('button', { name: /probe address/i })); await waitFor(() => - expect(onboardingMetricsTrackerMock.recordProbeResult).toHaveBeenCalledWith('no-match'), + expect( + screen.getByText(/No supported API-backed platform detected/i), + ).toBeInTheDocument(), ); fireEvent.click(screen.getByRole('button', { name: /Choose a source type instead/i })); @@ -132,8 +113,6 @@ describe('ConnectionEditor', () => { fireEvent.click(agentButton); expect(screen.getByTestId('slot').textContent).toBe('slot:agent'); - expect(onboardingMetricsTrackerMock.recordPathSelected).toHaveBeenCalledWith('agent'); - expect(onboardingMetricsTrackerMock.recordCredentialsOpened).toHaveBeenCalledWith('agent'); const call = renderSlot.mock.calls.at(-1)![0]; expect(call.type).toBe('agent'); expect(call.candidate).toBeNull(); @@ -166,32 +145,19 @@ describe('ConnectionEditor', () => { expect(screen.getByText('Address probe')).toBeInTheDocument(); }); - it('uses an injected tracker for direct type routes without creating another one', async () => { - const externalTracker = { - recordOpened: vi.fn(), - recordPathSelected: vi.fn(), - recordProbeResult: vi.fn(), - recordCatalogSelected: vi.fn(), - recordCredentialsOpened: vi.fn(), - }; + it('opens direct type routes without the detect setup', async () => { const renderSlot = vi.fn((props) =>
slot:{props.type}
); render(() => ( {}} /> )); await waitFor(() => expect(screen.getByTestId('slot').textContent).toBe('slot:truenas')); - expect(createInfrastructureOnboardingMetricsTrackerMock).not.toHaveBeenCalled(); - expect(externalTracker.recordOpened).not.toHaveBeenCalled(); - expect(externalTracker.recordPathSelected).toHaveBeenCalledWith('api'); - expect(externalTracker.recordCatalogSelected).toHaveBeenCalledWith('truenas'); - expect(externalTracker.recordCredentialsOpened).toHaveBeenCalledWith('truenas'); + expect(screen.queryByRole('button', { name: /probe address/i })).not.toBeInTheDocument(); }); it('skips probe setup when an initialType is supplied in edit mode', () => { @@ -206,7 +172,6 @@ describe('ConnectionEditor', () => { /> )); - expect(createInfrastructureOnboardingMetricsTrackerMock).not.toHaveBeenCalled(); expect(screen.getByTestId('slot').textContent).toBe('slot:vmware'); const call = renderSlot.mock.calls.at(0)![0]; expect(call.mode).toBe('edit'); diff --git a/frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx b/frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx index d6a39f752..efa6a6a04 100644 --- a/frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx +++ b/frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx @@ -10,12 +10,9 @@ import { getUnifiedAgentClipboardCopyErrorMessage, getUnifiedAgentClipboardCopySuccessMessage, } from '@/utils/unifiedAgentInventoryPresentation'; -import { trackAgentInstallCommandCopied } from '@/utils/upgradeMetrics'; import { INSTALL_PROFILE_OPTIONS, - normalizeTelemetryPart, type InstallProfile, - UNIFIED_AGENT_TELEMETRY_SURFACE, } from './infrastructureOperationsModel'; import { useInfrastructureOperationsContext } from './useInfrastructureOperationsState'; @@ -481,10 +478,6 @@ export const InfrastructureInstallerSection: Component = () => { {(snippet) => { const copyCommand = () => snippet.command; - const commandTelemetryCapability = () => { - const label = normalizeTelemetryPart(snippet.label) || 'install'; - return `${section.platform}:${state.installProfile()}:${label}`; - }; return (
@@ -497,10 +490,6 @@ export const InfrastructureInstallerSection: Component = () => { onClick={async () => { const success = await copyToClipboard(copyCommand()); if (success) { - trackAgentInstallCommandCopied( - UNIFIED_AGENT_TELEMETRY_SURFACE, - commandTelemetryCapability(), - ); notificationStore.success( getUnifiedAgentClipboardCopySuccessMessage(), ); diff --git a/frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx b/frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx index d6853985a..fa52f902d 100644 --- a/frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx +++ b/frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx @@ -41,11 +41,6 @@ import { InfrastructureOperationsStateProvider, useInfrastructureOperationsContext, } from './useInfrastructureOperationsState'; -import { - clearSharedInfrastructureOnboardingMetricsTracker, - getSharedInfrastructureOnboardingMetricsTracker, - type InfrastructureOnboardingMetricsTracker, -} from '@/utils/infrastructureOnboardingMetrics'; import { getInfrastructureOnboardingProductPresentation, getInfrastructureSourceStrategyPresentation, @@ -74,12 +69,6 @@ const closeButtonClass = const buttonClass = 'inline-flex min-h-10 sm:min-h-9 items-center justify-center rounded-md border border-border px-3 py-2 text-sm font-medium text-base-content transition-colors hover:bg-surface-hover disabled:cursor-not-allowed disabled:opacity-60'; -const createOpenedOnboardingTracker = (): InfrastructureOnboardingMetricsTracker => { - const tracker = getSharedInfrastructureOnboardingMetricsTracker(); - tracker.recordOpened(); - return tracker; -}; - const describeManagedSourceType = (type: ConnectionType | null): string => { if (!type) return 'Infrastructure'; if ( @@ -119,11 +108,6 @@ const InfrastructureWorkspaceContent: Component = if (readOnly()) return null; return deriveAddStepFromLocation(location.pathname, location.search ?? ''); }); - const [addFlowTracker, setAddFlowTracker] = - createSignal( - routeStep() ? createOpenedOnboardingTracker() : null, - ); - let previousRouteStep: InfrastructurePanelStep | null | undefined = routeStep(); const activeAddType = createMemo(() => { const step = routeStep(); if (!step || step === 'pick' || step === 'detect') return null; @@ -198,27 +182,18 @@ const InfrastructureWorkspaceContent: Component = const openAddFlow = (step: InfrastructurePanelStep) => { setSelectedDiscoveredSource(null); setSelectedProbeCandidate(null); - if (!addFlowTracker()) { - setAddFlowTracker(createOpenedOnboardingTracker()); - } navigate(buildInfrastructureOnboardingPath(step), { scroll: false }); }; const openAddFlowFromProbe = (candidate: ProbeCandidate) => { setSelectedDiscoveredSource(null); setSelectedProbeCandidate(candidate); - if (!addFlowTracker()) { - setAddFlowTracker(createOpenedOnboardingTracker()); - } navigate(buildInfrastructureOnboardingPath(candidate.type as ManagedAddTypeStep), { scroll: false, }); }; const reviewDiscoveredSource = (server: DiscoveredServer) => { - if (!addFlowTracker()) { - setAddFlowTracker(createOpenedOnboardingTracker()); - } setSelectedDiscoveredSource(server); navigate(buildInfrastructureOnboardingPath(server.type), { scroll: false }); }; @@ -227,8 +202,6 @@ const InfrastructureWorkspaceContent: Component = resetInlineEditorState(); setSelectedDiscoveredSource(null); setSelectedProbeCandidate(null); - setAddFlowTracker(null); - clearSharedInfrastructureOnboardingMetricsTracker(); navigateToWorkspace(Boolean(routeStep())); }; @@ -255,33 +228,12 @@ const InfrastructureWorkspaceContent: Component = closeEditFlow(); }; - const recordCatalogSelection = (type: InfrastructureOnboardingConnectionType) => { - const tracker = addFlowTracker(); - if (!tracker) return; - tracker.recordPathSelected(type === 'agent' ? 'agent' : 'api'); - if (type !== 'agent') { - tracker.recordCatalogSelected(type); - } - }; - createEffect(() => { if (activeAddType() !== 'agent') { setShowAgentProfiles(false); } }); - createEffect(() => { - const step = routeStep(); - const tracker = addFlowTracker(); - if (step && !tracker) { - setAddFlowTracker(createOpenedOnboardingTracker()); - } else if (!step && tracker && previousRouteStep) { - setAddFlowTracker(null); - clearSharedInfrastructureOnboardingMetricsTracker(); - } - previousRouteStep = step; - }); - createEffect(() => { if (!editingConnection()) return; if (ledger.loading()) return; @@ -950,7 +902,6 @@ const InfrastructureWorkspaceContent: Component = { - recordCatalogSelection(type); openAddFlow(type === 'agent' ? 'agent' : (type as ManagedAddTypeStep)); }} onDetectFromAddress={() => openAddFlow('detect')} @@ -960,7 +911,6 @@ const InfrastructureWorkspaceContent: Component = openAddFlow('pick')} onSelectAgentRoute={() => openAddFlow('agent')} onSelectCandidate={openAddFlowFromProbe} @@ -978,12 +928,6 @@ const InfrastructureWorkspaceContent: Component = initialType={activeAddType() ?? undefined} initialCandidate={selectedProbeCandidate()} showSlotHeader={false} - trackInitialCatalogSelection={ - activeAddType() !== 'agent' && - !selectedDiscoveredSource() && - !selectedProbeCandidate() - } - onboardingMetricsTracker={addFlowTracker()} onBackToCatalog={() => openAddFlow('pick')} onClose={closeAddFlow} onSaved={handleAddSaved} diff --git a/frontend-modern/src/components/Settings/ProLicensePanel.tsx b/frontend-modern/src/components/Settings/ProLicensePanel.tsx index 034a353f5..c7426f9fd 100644 --- a/frontend-modern/src/components/Settings/ProLicensePanel.tsx +++ b/frontend-modern/src/components/Settings/ProLicensePanel.tsx @@ -111,9 +111,7 @@ const ProLicensePanelContent: Component = () => { onReload={() => void state.loadPanelData()} planSelectionPrompt={state.planSelectionPrompt()} planComparisonSummary={state.planComparisonSummary()} - onPlanSelectionPromptClick={state.handlePlanSelectionPromptClick} purchaseActivationAction={state.purchaseActivationAction()} - onPurchaseActivationActionClick={state.handlePurchaseActivationActionClick} purchaseActivationNotice={state.purchaseActivationNotice()} /> diff --git a/frontend-modern/src/components/Settings/ProLicensePlanSection.tsx b/frontend-modern/src/components/Settings/ProLicensePlanSection.tsx index d872951df..6f0762f50 100644 --- a/frontend-modern/src/components/Settings/ProLicensePlanSection.tsx +++ b/frontend-modern/src/components/Settings/ProLicensePlanSection.tsx @@ -89,13 +89,11 @@ interface ProLicensePlanSectionProps { } | null; }; planSelectionPrompt: ActionNotice | null; - onPlanSelectionPromptClick: () => void; purchaseActivationNotice: Notice | null; purchaseActivationAction: { label: string; destination: UpgradeDestination; } | null; - onPurchaseActivationActionClick: () => void; } const formatDate = (value?: string | null) => { @@ -154,7 +152,6 @@ export const ProLicensePlanSection: Component = (pro {action().label} @@ -171,7 +168,6 @@ export const ProLicensePlanSection: Component = (pro {prompt().actionLabel} @@ -295,7 +291,6 @@ export const ProLicensePlanSection: Component = (pro {action().label} diff --git a/frontend-modern/src/components/Settings/RBACFeatureGateSection.tsx b/frontend-modern/src/components/Settings/RBACFeatureGateSection.tsx index bb98d5d09..858b48564 100644 --- a/frontend-modern/src/components/Settings/RBACFeatureGateSection.tsx +++ b/frontend-modern/src/components/Settings/RBACFeatureGateSection.tsx @@ -2,7 +2,6 @@ import { Component, Show } from 'solid-js'; import { UpgradeLink } from '@/components/shared/UpgradeLink'; import { getUpgradeActionDestination } from '@/stores/licenseCommercial'; import type { RBACFeatureGateCopy } from '@/utils/rbacPresentation'; -import { trackUpgradeClicked } from '@/utils/upgradeMetrics'; import { getUpgradeActionButtonClass, UPGRADE_ACTION_LABEL, @@ -27,7 +26,6 @@ export const RBACFeatureGateSection: Component = (p trackUpgradeClicked(props.paywallLocation, 'rbac')} > {UPGRADE_ACTION_LABEL} diff --git a/frontend-modern/src/components/Settings/RelaySettingsPanel.tsx b/frontend-modern/src/components/Settings/RelaySettingsPanel.tsx index b2d755165..5927c2a79 100644 --- a/frontend-modern/src/components/Settings/RelaySettingsPanel.tsx +++ b/frontend-modern/src/components/Settings/RelaySettingsPanel.tsx @@ -6,7 +6,6 @@ import { Toggle } from '@/components/shared/Toggle'; import { Card } from '@/components/shared/Card'; import { UpgradeLink } from '@/components/shared/UpgradeLink'; import { formField, labelClass, controlClass, formHelpText } from '@/components/shared/Form'; -import { trackUpgradeClicked } from '@/utils/upgradeMetrics'; import { getSettingsConfigurationLoadingState } from '@/utils/settingsShellPresentation'; import { RELAY_ENABLE_HELP_TEXT, @@ -49,7 +48,6 @@ export const RelaySettingsPanel: Component = (props) => trackUpgradeClicked('settings_relay_panel', 'relay')} > View plans diff --git a/frontend-modern/src/components/Settings/ReportingPanel.tsx b/frontend-modern/src/components/Settings/ReportingPanel.tsx index d1e29a05a..391b54b35 100644 --- a/frontend-modern/src/components/Settings/ReportingPanel.tsx +++ b/frontend-modern/src/components/Settings/ReportingPanel.tsx @@ -11,7 +11,6 @@ import { UpgradeLink } from '@/components/shared/UpgradeLink'; import { useReportingPanelState } from '@/components/Settings/useReportingPanelState'; import type { ReportingFormat } from '@/components/Settings/reportingCatalogModel'; import { type ReportingRangeValue } from '@/components/Settings/reportingPanelModel'; -import { trackUpgradeClicked } from '@/utils/upgradeMetrics'; import { getUpgradeActionButtonClass, UPGRADE_ACTION_LABEL, @@ -153,9 +152,6 @@ export function ReportingPanel() { - trackUpgradeClicked('settings_reporting_panel', reportingCatalog()!.id) - } > {UPGRADE_ACTION_LABEL} diff --git a/frontend-modern/src/components/Settings/SSOProvidersPanel.tsx b/frontend-modern/src/components/Settings/SSOProvidersPanel.tsx index c5539b7f1..fc6eaec9a 100644 --- a/frontend-modern/src/components/Settings/SSOProvidersPanel.tsx +++ b/frontend-modern/src/components/Settings/SSOProvidersPanel.tsx @@ -26,6 +26,7 @@ import { getSSOProviderSummary, getSSOProviderTypeBadgeClass, getSSOProviderTypeLabel, + getSSOSamlFeatureGateCopy, } from '@/utils/ssoProviderPresentation'; import { getUpgradeActionButtonClass, @@ -40,6 +41,7 @@ interface SSOProvidersPanelProps { } export const SSOProvidersPanel: Component = (props) => { + const samlFeatureGateCopy = getSSOSamlFeatureGateCopy(); const { providers, loading, @@ -78,7 +80,6 @@ export const SSOProvidersPanel: Component = (props) => { getUpgradeActionDestination, runtimeCapabilitiesLoaded, showUpgradePrompts, - trackUpgradeClicked, } = useSSOProvidersState(props); return ( @@ -95,7 +96,7 @@ export const SSOProvidersPanel: Component = (props) => {

Add SAML Provider

-

Pro feature

+

{samlFeatureGateCopy.subtitle}

-

SAML 2.0 and multi-provider SSO requires Pro.

+

{samlFeatureGateCopy.body}

diff --git a/frontend-modern/src/components/Settings/__tests__/AISettings.test.tsx b/frontend-modern/src/components/Settings/__tests__/AISettings.test.tsx index c2852dda0..58a0c6128 100644 --- a/frontend-modern/src/components/Settings/__tests__/AISettings.test.tsx +++ b/frontend-modern/src/components/Settings/__tests__/AISettings.test.tsx @@ -25,8 +25,6 @@ const loadLicenseStatusMock = vi.fn(); const loadCommercialPostureMock = vi.fn(); const commercialPostureMock = vi.fn(); const entitlementsMock = vi.fn(); -const trackPaywallViewedMock = vi.fn(); -const trackUpgradeClickedMock = vi.fn(); const presentationPolicyHidesUpgradePromptsMock = vi.fn(); vi.mock('@/api/ai', () => ({ @@ -81,11 +79,6 @@ vi.mock('@/stores/sessionPresentationPolicy', () => ({ presentationPolicyHidesUpgradePrompts: () => presentationPolicyHidesUpgradePromptsMock(), })); -vi.mock('@/utils/upgradeMetrics', () => ({ - trackPaywallViewed: (...args: unknown[]) => trackPaywallViewedMock(...args), - trackUpgradeClicked: (...args: unknown[]) => trackUpgradeClickedMock(...args), -})); - const baseSettings = (): AISettingsType => ({ enabled: false, model: '', @@ -130,8 +123,6 @@ const resetAllMocks = () => { loadCommercialPostureMock.mockReset(); commercialPostureMock.mockReset(); entitlementsMock.mockReset(); - trackPaywallViewedMock.mockReset(); - trackUpgradeClickedMock.mockReset(); presentationPolicyHidesUpgradePromptsMock.mockReset(); }; @@ -341,7 +332,6 @@ describe('AISettings model loading error states', () => { }); expect(screen.queryByRole('option', { name: /Autonomous/i })).not.toBeInTheDocument(); - expect(trackPaywallViewedMock).not.toHaveBeenCalled(); }); it('keeps an existing autonomous setting visible even when upgrade prompts are hidden', async () => { diff --git a/frontend-modern/src/components/Settings/__tests__/AgentProfilesPanel.test.tsx b/frontend-modern/src/components/Settings/__tests__/AgentProfilesPanel.test.tsx index 3d95b48f9..72f3e84f6 100644 --- a/frontend-modern/src/components/Settings/__tests__/AgentProfilesPanel.test.tsx +++ b/frontend-modern/src/components/Settings/__tests__/AgentProfilesPanel.test.tsx @@ -90,11 +90,6 @@ vi.mock('@/utils/logger', () => ({ }, })); -vi.mock('@/utils/upgradeMetrics', () => ({ - trackPaywallViewed: vi.fn(), - trackUpgradeClicked: vi.fn(), -})); - const makeAgentResource = (overrides: Partial = {}): Resource => ({ id: 'hash-agent-resource-id', type: 'agent', diff --git a/frontend-modern/src/components/Settings/__tests__/InfrastructureOperationsModel.test.tsx b/frontend-modern/src/components/Settings/__tests__/InfrastructureOperationsModel.test.tsx index eab29b81e..8a73e94d6 100644 --- a/frontend-modern/src/components/Settings/__tests__/InfrastructureOperationsModel.test.tsx +++ b/frontend-modern/src/components/Settings/__tests__/InfrastructureOperationsModel.test.tsx @@ -2,6 +2,8 @@ import { describe, expect, it } from 'vitest'; import type { ConnectedInfrastructureItem } from '@/types/api'; import type { UnifiedAgentRow } from '../infrastructureOperationsModel'; import infrastructureInstallerSectionSource from '../InfrastructureInstallerSection.tsx?raw'; +import infrastructureOperationsModelSource from '../infrastructureOperationsModel.tsx?raw'; +import useInfrastructureInstallStateSource from '../useInfrastructureInstallState.tsx?raw'; import { INSTALL_PROFILE_OPTIONS, getCapabilityManagementPath, @@ -230,6 +232,24 @@ describe('infrastructure operations model', () => { expect(installStateSource).not.toContain("navigate('/dashboard')"); }); + it('keeps infrastructure install and operations surfaces free of retired commercial telemetry wrappers', () => { + for (const source of [ + infrastructureOperationsModelSource, + infrastructureInstallerSectionSource, + useInfrastructureInstallStateSource, + ]) { + expect(source).not.toContain('upgradeMetrics'); + expect(source).not.toContain('conversionEvents'); + expect(source).not.toContain('infrastructureOnboardingMetrics'); + expect(source).not.toContain('UNIFIED_AGENT_TELEMETRY_SURFACE'); + expect(source).not.toContain('normalizeTelemetryPart'); + expect(source).not.toContain('trackAgentInstallTokenCreated'); + expect(source).not.toContain('trackAgentInstallCommandsCopied'); + expect(source).not.toContain('trackAgentFirstConnected'); + expect(source).not.toContain('/api/upgrade-metrics/events'); + } + }); + it('does not reintroduce the retired reporting state hook on the operations state', async () => { const operationsStateSource = await import( '../useInfrastructureOperationsState?raw' diff --git a/frontend-modern/src/components/Settings/__tests__/InfrastructureWorkspace.test.tsx b/frontend-modern/src/components/Settings/__tests__/InfrastructureWorkspace.test.tsx index a5bf44b20..8ce56ee27 100644 --- a/frontend-modern/src/components/Settings/__tests__/InfrastructureWorkspace.test.tsx +++ b/frontend-modern/src/components/Settings/__tests__/InfrastructureWorkspace.test.tsx @@ -31,29 +31,6 @@ const emptyFleetMember = vi.hoisted( ); const navigateSpy = vi.hoisted(() => vi.fn()); const presentationPolicyIsReadOnlyMock = vi.hoisted(() => vi.fn(() => false)); -const onboardingMetricsTrackers = vi.hoisted( - () => - [] as Array<{ - recordOpened: ReturnType; - recordPathSelected: ReturnType; - recordProbeResult: ReturnType; - recordCatalogSelected: ReturnType; - recordCredentialsOpened: ReturnType; - }>, -); -const createInfrastructureOnboardingMetricsTrackerMock = vi.hoisted(() => - vi.fn(() => { - const tracker = { - recordOpened: vi.fn(), - recordPathSelected: vi.fn(), - recordProbeResult: vi.fn(), - recordCatalogSelected: vi.fn(), - recordCredentialsOpened: vi.fn(), - }; - onboardingMetricsTrackers.push(tracker); - return tracker; - }), -); const originalResizeObserver = globalThis.ResizeObserver; let latestResizeObserverCallback: ResizeObserverCallback | null = null; @@ -179,12 +156,6 @@ vi.mock('../AgentProfilesPanel', () => ({ AgentProfilesPanel: () =>
profiles
, })); -vi.mock('@/utils/infrastructureOnboardingMetrics', () => ({ - createInfrastructureOnboardingMetricsTracker: createInfrastructureOnboardingMetricsTrackerMock, - getSharedInfrastructureOnboardingMetricsTracker: createInfrastructureOnboardingMetricsTrackerMock, - clearSharedInfrastructureOnboardingMetricsTracker: vi.fn(), -})); - const connectionFixture = (overrides: Partial = {}): Connection => ({ id: 'pve:zeus', type: 'pve', @@ -280,8 +251,6 @@ describe('InfrastructureWorkspace', () => { navigateSpy.mockReset(); presentationPolicyIsReadOnlyMock.mockReset(); presentationPolicyIsReadOnlyMock.mockReturnValue(false); - createInfrastructureOnboardingMetricsTrackerMock.mockClear(); - onboardingMetricsTrackers.length = 0; routeState.pathname = '/settings/infrastructure'; routeState.search = ''; connectionState.connections = [connectionFixture()]; @@ -624,8 +593,6 @@ describe('InfrastructureWorkspace', () => { expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure?add=pve', { scroll: false, }); - expect(createInfrastructureOnboardingMetricsTrackerMock).toHaveBeenCalledTimes(1); - expect(onboardingMetricsTrackers[0]?.recordOpened).toHaveBeenCalledTimes(1); }); it('opens the platform picker from the Add infrastructure action', () => { @@ -715,14 +682,12 @@ describe('InfrastructureWorkspace', () => { expect(screen.getByTestId('install-section')).toBeInTheDocument(); }); - it('creates one onboarding tracker for a direct type route before the add dialog mounts', async () => { + it('renders a direct type route before the add dialog mounts', async () => { routeState.search = '?add=truenas'; renderWorkspace(); await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument()); - expect(createInfrastructureOnboardingMetricsTrackerMock).toHaveBeenCalledTimes(1); - expect(onboardingMetricsTrackers).toHaveLength(1); - expect(onboardingMetricsTrackers[0]?.recordOpened).toHaveBeenCalledTimes(1); + expect(screen.getByTestId('truenas-section')).toBeInTheDocument(); }); it('opens the manage dialog directly from an existing source card', async () => { diff --git a/frontend-modern/src/components/Settings/__tests__/ProLicensePanel.test.tsx b/frontend-modern/src/components/Settings/__tests__/ProLicensePanel.test.tsx index 61f71c55d..b68c72e7f 100644 --- a/frontend-modern/src/components/Settings/__tests__/ProLicensePanel.test.tsx +++ b/frontend-modern/src/components/Settings/__tests__/ProLicensePanel.test.tsx @@ -28,8 +28,6 @@ import { let mockEntitlements: LicenseEntitlements | null = null; -const trackPricingViewedMock = vi.fn(); -const trackCheckoutClickedMock = vi.fn(); const loadRuntimeLicenseStatusMock = vi.fn(); const loadCommercialPostureMock = vi.fn(); const loadLicenseEntitlementsMock = vi.fn(); @@ -98,11 +96,6 @@ vi.mock('@/stores/notifications', () => ({ }, })); -vi.mock('@/utils/upgradeMetrics', () => ({ - trackPricingViewed: (...args: unknown[]) => trackPricingViewedMock(...args), - trackCheckoutClicked: (...args: unknown[]) => trackCheckoutClickedMock(...args), -})); - describe('ProLicensePanel', () => { const renderPanel = () => render(() => ( @@ -124,8 +117,6 @@ describe('ProLicensePanel', () => { loadRuntimeLicenseStatusMock.mockReset(); loadCommercialPostureMock.mockReset(); loadLicenseEntitlementsMock.mockReset(); - trackPricingViewedMock.mockReset(); - trackCheckoutClickedMock.mockReset(); licenseEntitlementsLoadErrorMock.mockReset(); startProTrialMock.mockReset(); activateLicenseMock.mockReset(); @@ -228,10 +219,9 @@ describe('ProLicensePanel', () => { expect(screen.queryByText('What Relay adds')).not.toBeInTheDocument(); expect(screen.queryByText('What Pulse Pro adds')).not.toBeInTheDocument(); expect(screen.queryByRole('link', { name: 'Compare plans' })).not.toBeInTheDocument(); - expect(trackPricingViewedMock).not.toHaveBeenCalled(); }); - it('tracks compare-plan checkout intent from the explicit self-hosted billing handoff', async () => { + it('opens compare-plan checkout from the explicit self-hosted billing handoff', async () => { useLocationMock.mockReturnValue({ search: `?intent=${SELF_HOSTED_PRO_BILLING_PLAN_SELECTION_INTENT}`, pathname: '/settings/system/billing/plan', @@ -245,11 +235,6 @@ describe('ProLicensePanel', () => { }); await fireEvent.click(screen.getAllByRole('link', { name: 'Compare plans' })[0]); - - expect(trackCheckoutClickedMock).toHaveBeenCalledWith( - 'settings_self_hosted_billing_compare_prompt', - SELF_HOSTED_PRO_BILLING_PLAN_SELECTION_INTENT, - ); }); it('keeps explicit self-hosted plan comparison focused on optional paid extras', async () => { diff --git a/frontend-modern/src/components/Settings/__tests__/RBACPaywallPanels.test.tsx b/frontend-modern/src/components/Settings/__tests__/RBACPaywallPanels.test.tsx index fdb0e6b55..a24eec576 100644 --- a/frontend-modern/src/components/Settings/__tests__/RBACPaywallPanels.test.tsx +++ b/frontend-modern/src/components/Settings/__tests__/RBACPaywallPanels.test.tsx @@ -11,8 +11,6 @@ const startProTrialMock = vi.fn(); const commercialPostureMock = vi.fn(); const entitlementsMock = vi.fn(); const presentationPolicyHidesUpgradePromptsMock = vi.fn(); -const trackPaywallViewedMock = vi.fn(); -const trackUpgradeClickedMock = vi.fn(); const getRolesMock = vi.fn(); const getUsersMock = vi.fn(); const getUserPermissionsMock = vi.fn(); @@ -45,11 +43,6 @@ vi.mock('@/stores/sessionPresentationPolicy', () => ({ presentationPolicyHidesUpgradePrompts: () => presentationPolicyHidesUpgradePromptsMock(), })); -vi.mock('@/utils/upgradeMetrics', () => ({ - trackPaywallViewed: (...args: unknown[]) => trackPaywallViewedMock(...args), - trackUpgradeClicked: (...args: unknown[]) => trackUpgradeClickedMock(...args), -})); - vi.mock('@/api/rbac', () => ({ RBACAPI: { getRoles: (...args: unknown[]) => getRolesMock(...args), @@ -85,8 +78,6 @@ describe('RBAC paywall settings panels', () => { commercialPostureMock.mockReset(); entitlementsMock.mockReset(); presentationPolicyHidesUpgradePromptsMock.mockReset(); - trackPaywallViewedMock.mockReset(); - trackUpgradeClickedMock.mockReset(); getRolesMock.mockReset(); getUsersMock.mockReset(); getUserPermissionsMock.mockReset(); @@ -131,7 +122,7 @@ describe('RBAC paywall settings panels', () => { render(() => ); await waitFor(() => { - expect(screen.getByText('Custom Roles (Pro)')).toBeInTheDocument(); + expect(screen.getByText('Custom Roles')).toBeInTheDocument(); }); expect(screen.getByRole('link', { name: 'View plans' })).toHaveAttribute( @@ -140,7 +131,6 @@ describe('RBAC paywall settings panels', () => { ); expect(screen.getByRole('button', { name: 'New Role' })).toBeDisabled(); expect(getRolesMock).not.toHaveBeenCalled(); - expect(trackPaywallViewedMock).toHaveBeenCalledWith('rbac', 'settings_roles_panel'); }); it('keeps RBAC upgrade actions quiet when self-hosted upgrade prompts are hidden', async () => { @@ -156,7 +146,6 @@ describe('RBAC paywall settings panels', () => { expect(screen.queryByText('Custom Roles (Pro)')).not.toBeInTheDocument(); expect(screen.queryByRole('link', { name: 'View plans' })).not.toBeInTheDocument(); expect(screen.queryByRole('button', { name: 'Start trial' })).not.toBeInTheDocument(); - expect(trackPaywallViewedMock).not.toHaveBeenCalled(); expect(getRolesMock).not.toHaveBeenCalled(); }); @@ -169,7 +158,6 @@ describe('RBAC paywall settings panels', () => { expect(screen.queryByText('Custom Roles (Pro)')).not.toBeInTheDocument(); expect(getRolesMock).toHaveBeenCalled(); - expect(trackPaywallViewedMock).not.toHaveBeenCalled(); expect(screen.getByRole('button', { name: 'New Role' })).not.toBeDisabled(); }); @@ -179,7 +167,7 @@ describe('RBAC paywall settings panels', () => { render(() => ); await waitFor(() => { - expect(screen.getByText('Centralized Access Control (Pro)')).toBeInTheDocument(); + expect(screen.getByText('Centralized Access Control')).toBeInTheDocument(); }); expect(screen.getByRole('link', { name: 'View plans' })).toHaveAttribute( @@ -189,7 +177,6 @@ describe('RBAC paywall settings panels', () => { expect(screen.getByPlaceholderText('Search users...')).toBeDisabled(); expect(getUsersMock).not.toHaveBeenCalled(); expect(getRolesMock).not.toHaveBeenCalled(); - expect(trackPaywallViewedMock).toHaveBeenCalledWith('rbac', 'settings_user_assignments_panel'); }); it('loads user assignments when the RBAC entitlement is granted', async () => { @@ -202,7 +189,6 @@ describe('RBAC paywall settings panels', () => { expect(screen.queryByText('Centralized Access Control (Pro)')).not.toBeInTheDocument(); expect(getUsersMock).toHaveBeenCalled(); expect(getRolesMock).toHaveBeenCalled(); - expect(trackPaywallViewedMock).not.toHaveBeenCalled(); expect(screen.getByPlaceholderText('Search users...')).not.toBeDisabled(); }); @@ -219,7 +205,6 @@ describe('RBAC paywall settings panels', () => { expect(screen.queryByText('Centralized Access Control (Pro)')).not.toBeInTheDocument(); expect(screen.queryByRole('link', { name: 'View plans' })).not.toBeInTheDocument(); expect(screen.queryByRole('button', { name: 'Start trial' })).not.toBeInTheDocument(); - expect(trackPaywallViewedMock).not.toHaveBeenCalled(); expect(getUsersMock).not.toHaveBeenCalled(); expect(getRolesMock).not.toHaveBeenCalled(); }); @@ -262,7 +247,7 @@ describe('RBAC paywall settings panels', () => { render(() => ); await waitFor(() => { - expect(screen.getByText('Custom Roles (Pro)')).toBeInTheDocument(); + expect(screen.getByText('Custom Roles')).toBeInTheDocument(); }); expect(screen.getByRole('link', { name: 'View plans' })).toBeInTheDocument(); diff --git a/frontend-modern/src/components/Settings/__tests__/ReportingPanel.test.tsx b/frontend-modern/src/components/Settings/__tests__/ReportingPanel.test.tsx index 7032a7d39..9fd87e53f 100644 --- a/frontend-modern/src/components/Settings/__tests__/ReportingPanel.test.tsx +++ b/frontend-modern/src/components/Settings/__tests__/ReportingPanel.test.tsx @@ -18,16 +18,12 @@ vi.mock('../ResourcePicker', () => ({ }): JSX.Element =>
Mock Resource Picker
, })); -vi.mock('@/utils/upgradeMetrics', () => ({ - trackUpgradeClicked: vi.fn(), -})); - const baseCatalog = { id: 'advanced_reporting', title: 'Detailed Reporting', description: 'Canonical reporting surfaces', lockedState: { - title: 'Advanced Reporting (Pro)', + title: 'Advanced Reporting', description: 'Canonical locked reporting teaser', }, guidance: { diff --git a/frontend-modern/src/components/Settings/__tests__/reportingCatalogModel.test.ts b/frontend-modern/src/components/Settings/__tests__/reportingCatalogModel.test.ts index 55dbf7a03..e2c72fb62 100644 --- a/frontend-modern/src/components/Settings/__tests__/reportingCatalogModel.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/reportingCatalogModel.test.ts @@ -10,7 +10,7 @@ const baseCatalogPayload = { title: 'Detailed Reporting', description: 'Canonical reporting surfaces', lockedState: { - title: 'Advanced Reporting (Pro)', + title: 'Advanced Reporting', description: 'Canonical locked reporting teaser', }, guidance: { @@ -81,7 +81,7 @@ describe('reporting catalog model', () => { it('parses the canonical reporting catalog payload', () => { const catalog = parseReportingCatalog(baseCatalogPayload); - expect(catalog.lockedState.title).toBe('Advanced Reporting (Pro)'); + expect(catalog.lockedState.title).toBe('Advanced Reporting'); expect(catalog.guidance.title).toBe('Advanced Insights'); expect(catalog.performanceReport.defaultFormat).toBe('pdf'); expect(catalog.performanceReport.ranges[0].windowHours).toBe(24); @@ -153,7 +153,7 @@ describe('reporting catalog model', () => { parseReportingCatalog({ ...baseCatalogPayload, lockedState: { - title: 'Advanced Reporting (Pro)', + title: 'Advanced Reporting', }, }), ).toThrow('Invalid reporting catalog payload'); diff --git a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts index 78b6c1a95..03c2b459f 100644 --- a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts @@ -22,6 +22,7 @@ import dataHandlingPanelSource from '../DataHandlingPanel.tsx?raw'; import auditLogPanelSource from '../AuditLogPanel.tsx?raw'; import auditWebhookPanelSource from '../AuditWebhookPanel.tsx?raw'; import reportingPanelSource from '../ReportingPanel.tsx?raw'; +import rbacFeatureGateSectionSource from '../RBACFeatureGateSection.tsx?raw'; import recoverySettingsPanelSource from '../RecoverySettingsPanel.tsx?raw'; import systemLogsPanelSource from '../SystemLogsPanel.tsx?raw'; import updatesSettingsPanelSource from '../UpdatesSettingsPanel.tsx?raw'; @@ -54,10 +55,18 @@ import organizationSharingCreateSectionSource from '../OrganizationSharingCreate import rolesEditorDialogSource from '../RolesEditorDialog.tsx?raw'; import diagnosticsResultsPanelSource from '../DiagnosticsResultsPanel.tsx?raw'; import diagnosticsModelSource from '../diagnosticsModel.ts?raw'; +import agentProfilesStateSource from '../useAgentProfilesPanelState.ts?raw'; +import auditLogStateSource from '../useAuditLogPanelState.ts?raw'; +import auditWebhookStateSource from '../useAuditWebhookPanelState.ts?raw'; +import rbacFeatureGateStateSource from '../useRBACFeatureGateState.ts?raw'; +import reportingStateSource from '../useReportingPanelState.ts?raw'; +import ssoProvidersPanelSource from '../SSOProvidersPanel.tsx?raw'; +import ssoProvidersStateSource from '../useSSOProvidersState.ts?raw'; import infrastructureOnboardingPresentationSource from '../../../utils/infrastructureOnboardingPresentation.ts?raw'; import selfHostedBillingPresentationSource from '../selfHostedBillingPresentation.ts?raw'; import systemSettingsPresentationSource from '../../../utils/systemSettingsPresentation.ts?raw'; import auditLogPresentationSource from '../../../utils/auditLogPresentation.ts?raw'; +import monitoredSystemLimitWarningBannerSource from '../../shared/MonitoredSystemLimitWarningBanner.tsx?raw'; const settingsRuntimeSources = import.meta.glob(['../*.tsx', '../ConnectionEditor/**/*.tsx'], { query: '?raw', @@ -197,6 +206,42 @@ describe('settings architecture guardrails', () => { expect(aiRuntimeControlsSectionSource).not.toContain('without approval (Pro)'); }); + it('keeps contextual settings feature gates free of retired commercial telemetry wrappers', () => { + for (const source of [ + agentProfilesPanelSource, + agentProfilesStateSource, + aiRuntimeControlsSectionSource, + auditLogPanelSource, + auditLogStateSource, + auditWebhookPanelSource, + auditWebhookStateSource, + monitoredSystemLimitWarningBannerSource, + rbacFeatureGateSectionSource, + rbacFeatureGateStateSource, + reportingPanelSource, + reportingStateSource, + ssoProvidersPanelSource, + ssoProvidersStateSource, + ]) { + expect(source).not.toContain('upgradeMetrics'); + expect(source).not.toContain('conversionEvents'); + expect(source).not.toContain('infrastructureOnboardingMetrics'); + expect(source).not.toContain('trackPaywallViewed'); + expect(source).not.toContain('trackUpgradeClicked'); + expect(source).not.toContain('/api/upgrade-metrics/events'); + } + + expect(agentProfilesPanelSource).not.toContain('Pro feature'); + expect(auditWebhookPanelSource).not.toContain('Audit Webhooks (Pro)'); + expect(reportingPanelSource).not.toContain('Advanced Reporting (Pro)'); + expect(rbacFeatureGateSectionSource).not.toContain('Custom Roles (Pro)'); + expect(rbacFeatureGateSectionSource).not.toContain('Centralized Access Control (Pro)'); + expect(ssoProvidersPanelSource).not.toContain('Add SAML (Pro)'); + expect(monitoredSystemLimitWarningBannerSource).not.toContain( + 'Unlimited self-hosted monitoring', + ); + }); + it('keeps Docker and Podman update-action copy on the system settings presentation owner', () => { expect(dockerRuntimeSettingsCardSource).toContain('DOCKER_UPDATE_ACTIONS_SECTION_TITLE'); expect(dockerRuntimeSettingsCardSource).toContain('DOCKER_UPDATE_ACTIONS_ENV_VAR'); @@ -366,12 +411,13 @@ describe('settings architecture guardrails', () => { expect(infrastructureWorkspaceSource).not.toContain(''); expect(infrastructureWorkspaceSource).toContain('flex h-full min-h-0 flex-col'); expect(infrastructureWorkspaceSource).toContain('showSlotHeader={false}'); - expect(infrastructureWorkspaceSource).toContain('trackInitialCatalogSelection={'); expect(infrastructureWorkspaceSource).toContain( "onDetectFromAddress={() => openAddFlow('detect')}", ); expect(infrastructureWorkspaceSource).toContain("onBackToCatalog={() => openAddFlow('pick')}"); - expect(infrastructureWorkspaceSource).toContain('recordCatalogSelection(type);'); + expect(infrastructureWorkspaceSource).not.toContain('trackInitialCatalogSelection'); + expect(infrastructureWorkspaceSource).not.toContain('recordCatalogSelection'); + expect(infrastructureWorkspaceSource).not.toContain('onboardingMetricsTracker'); expect(infrastructureWorkspaceSource).toContain('renderAgentConnectionDetails'); expect(infrastructureWorkspaceSource).not.toContain('InfrastructureOperationsController'); expect(infrastructureWorkspaceSource).not.toContain('PlatformConnectionsWorkspace'); diff --git a/frontend-modern/src/components/Settings/__tests__/settingsReadOnlyPanels.test.tsx b/frontend-modern/src/components/Settings/__tests__/settingsReadOnlyPanels.test.tsx index bcf3c4024..b4e861bea 100644 --- a/frontend-modern/src/components/Settings/__tests__/settingsReadOnlyPanels.test.tsx +++ b/frontend-modern/src/components/Settings/__tests__/settingsReadOnlyPanels.test.tsx @@ -16,8 +16,6 @@ const showErrorMock = vi.fn(); const showWarningMock = vi.fn(); const loggerErrorMock = vi.fn(); const presentationPolicyHidesUpgradePromptsMock = vi.fn(); -const trackPaywallViewedMock = vi.fn(); -const trackUpgradeClickedMock = vi.fn(); vi.mock('../QuickSecuritySetup', () => ({ QuickSecuritySetup: () =>
Quick Security Setup
, @@ -69,11 +67,6 @@ vi.mock('@/utils/logger', () => ({ }, })); -vi.mock('@/utils/upgradeMetrics', () => ({ - trackPaywallViewed: (...args: unknown[]) => trackPaywallViewedMock(...args), - trackUpgradeClicked: (...args: unknown[]) => trackUpgradeClickedMock(...args), -})); - describe('settings read-only panel states', () => { beforeEach(() => { hasFeatureMock.mockReset(); @@ -86,8 +79,6 @@ describe('settings read-only panel states', () => { showWarningMock.mockReset(); loggerErrorMock.mockReset(); presentationPolicyHidesUpgradePromptsMock.mockReset(); - trackPaywallViewedMock.mockReset(); - trackUpgradeClickedMock.mockReset(); hasFeatureMock.mockReturnValue(true); presentationPolicyHidesUpgradePromptsMock.mockReturnValue(false); @@ -180,7 +171,6 @@ describe('settings read-only panel states', () => { expect(screen.queryByText('Audit Webhooks (Pro)')).not.toBeInTheDocument(); expect(screen.queryByText(/require Pro/i)).not.toBeInTheDocument(); expect(screen.queryByRole('link', { name: 'View plans' })).not.toBeInTheDocument(); - expect(trackPaywallViewedMock).not.toHaveBeenCalled(); expect(apiFetchJSONMock).not.toHaveBeenCalled(); }); }); diff --git a/frontend-modern/src/components/Settings/__tests__/useReportingPanelState.test.ts b/frontend-modern/src/components/Settings/__tests__/useReportingPanelState.test.ts index 2d6e5b764..88490624e 100644 --- a/frontend-modern/src/components/Settings/__tests__/useReportingPanelState.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/useReportingPanelState.test.ts @@ -16,7 +16,7 @@ const catalogPayload = { title: 'Detailed Reporting', description: 'Canonical reporting surfaces', lockedState: { - title: 'Advanced Reporting (Pro)', + title: 'Advanced Reporting', description: 'Canonical locked reporting teaser', }, guidance: { @@ -105,10 +105,6 @@ describe('useReportingPanelState', () => { showWarning: vi.fn(), })); - vi.doMock('@/utils/upgradeMetrics', () => ({ - trackPaywallViewed: vi.fn(), - })); - vi.doMock('@/stores/license', () => ({ hasFeature: vi.fn( (feature: string) => feature === 'advanced_reporting' && hasReportingFeature, @@ -178,7 +174,7 @@ describe('useReportingPanelState', () => { expect(apiFetchMock).toHaveBeenCalledWith('/api/admin/reports/catalog'); expect(hookState.reportingCatalog()?.title).toBe('Detailed Reporting'); - expect(hookState.reportingCatalog()?.lockedState.title).toBe('Advanced Reporting (Pro)'); + expect(hookState.reportingCatalog()?.lockedState.title).toBe('Advanced Reporting'); expect(hookState.isLocked()).toBe(true); dispose(); @@ -207,10 +203,6 @@ describe('useReportingPanelState', () => { showWarning: vi.fn(), })); - vi.doMock('@/utils/upgradeMetrics', () => ({ - trackPaywallViewed: vi.fn(), - })); - vi.doMock('@/stores/license', () => ({ hasFeature: vi.fn(() => false), runtimeCapabilitiesLoaded: vi.fn(() => false), @@ -267,10 +259,6 @@ describe('useReportingPanelState', () => { showWarning: vi.fn(), })); - vi.doMock('@/utils/upgradeMetrics', () => ({ - trackPaywallViewed: vi.fn(), - })); - vi.doMock('@/stores/license', () => ({ hasFeature: vi.fn( (feature: string) => feature === 'advanced_reporting' && hasReportingFeature, @@ -333,10 +321,6 @@ describe('useReportingPanelState', () => { showWarning: vi.fn(), })); - vi.doMock('@/utils/upgradeMetrics', () => ({ - trackPaywallViewed: vi.fn(), - })); - vi.doMock('@/stores/license', () => ({ hasFeature: vi.fn( (feature: string) => feature === 'advanced_reporting' && hasReportingFeature, @@ -388,10 +372,6 @@ describe('useReportingPanelState', () => { showWarning: vi.fn(), })); - vi.doMock('@/utils/upgradeMetrics', () => ({ - trackPaywallViewed: vi.fn(), - })); - vi.doMock('@/stores/license', () => ({ hasFeature: vi.fn( (feature: string) => feature === 'advanced_reporting' && hasReportingFeature, diff --git a/frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx b/frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx index 5dab3593f..b70c33246 100644 --- a/frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx +++ b/frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx @@ -6,7 +6,6 @@ import { type PlatformConnectionsView } from './platformConnectionsModel'; import { buildInfrastructureWorkspacePath } from './infrastructureWorkspaceModel'; export const TOKEN_PLACEHOLDER = ''; -export const UNIFIED_AGENT_TELEMETRY_SURFACE = 'settings_unified_agents'; export type AgentPlatform = 'linux' | 'macos' | 'freebsd' | 'windows'; export type UnifiedAgentStatus = 'active' | 'removed'; @@ -111,12 +110,6 @@ export const buildDefaultTokenName = () => { return `Agent ${stamp}`; }; -export const normalizeTelemetryPart = (value: string) => - value - .toLowerCase() - .replace(/[^a-z0-9]+/g, '_') - .replace(/^_+|_+$/g, ''); - export const shellQuoteArg = (value: string) => `'${value.replace(/'/g, `'\"'\"'`)}'`; export const getCapabilitySurfaceLabel = (capability: AgentCapability) => { diff --git a/frontend-modern/src/components/Settings/reportingCatalogModel.ts b/frontend-modern/src/components/Settings/reportingCatalogModel.ts index 554afc977..d4aeb7df8 100644 --- a/frontend-modern/src/components/Settings/reportingCatalogModel.ts +++ b/frontend-modern/src/components/Settings/reportingCatalogModel.ts @@ -70,8 +70,9 @@ export function buildLegacyReportingCatalogFallback(): ReportingCatalog { title: 'Detailed Reporting', description: 'Generate performance reports across infrastructure and workloads.', lockedState: { - title: 'Advanced Reporting (Pro)', - description: 'Generate PDF and CSV performance reports across infrastructure and workload resources.', + title: 'Advanced Reporting', + description: + 'Generate PDF and CSV performance reports across infrastructure and workload resources on paid self-hosted and hosted plans.', }, guidance: { title: 'Advanced Insights', diff --git a/frontend-modern/src/components/Settings/settingsFeatureGates.ts b/frontend-modern/src/components/Settings/settingsFeatureGates.ts index c2cf2a334..7fb443a26 100644 --- a/frontend-modern/src/components/Settings/settingsFeatureGates.ts +++ b/frontend-modern/src/components/Settings/settingsFeatureGates.ts @@ -1,5 +1,4 @@ import type { SettingsTab } from './settingsNavigationModel'; -import { trackPaywallViewed } from '@/utils/upgradeMetrics'; import { getFeatureMinTierLabel } from '@/utils/licensePresentation'; export const tabFeatureRequirements: Partial> = { @@ -41,9 +40,6 @@ export function getTabLockReason( if (!runtimeCapabilitiesLoaded()) return null; if (requiredFeatures.every((feature) => hasFeature(feature))) return null; const primaryRequiredFeature = requiredFeatures[0]; - if (primaryRequiredFeature) { - trackPaywallViewed(primaryRequiredFeature, 'settings_tab'); - } const tierLabel = getFeatureMinTierLabel(primaryRequiredFeature); return `This settings section requires ${tierLabel}.`; } diff --git a/frontend-modern/src/components/Settings/useAISettingsState.ts b/frontend-modern/src/components/Settings/useAISettingsState.ts index 0fdfc82ae..b06b58f76 100644 --- a/frontend-modern/src/components/Settings/useAISettingsState.ts +++ b/frontend-modern/src/components/Settings/useAISettingsState.ts @@ -1,4 +1,4 @@ -import { createEffect, createMemo, createSignal, onMount } from 'solid-js'; +import { createMemo, createSignal, onMount } from 'solid-js'; import { createStore } from 'solid-js/store'; import { AIAPI } from '@/api/ai'; import { AIChatAPI, type ChatSession, type FileChange } from '@/api/aiChat'; @@ -36,7 +36,6 @@ import { getAISettingsToggleErrorMessage, } from '@/utils/aiSettingsPresentation'; import { logger } from '@/utils/logger'; -import { trackPaywallViewed } from '@/utils/upgradeMetrics'; export const useAISettingsState = () => { const [settings, setSettings] = createSignal(null); @@ -152,14 +151,6 @@ export const useAISettingsState = () => { clearAIRuntimeModels(); }; - createEffect((wasPaywallVisible) => { - const isPaywallVisible = form.controlLevel === 'autonomous' && autoFixLocked(); - if (showUpgradePrompts() && isPaywallVisible && !wasPaywallVisible) { - trackPaywallViewed('ai_autofix', 'settings_ai_patrol_autofix'); - } - return isPaywallVisible; - }, false); - const resetForm = (data: AISettingsType | null) => { if (!data) { setForm({ diff --git a/frontend-modern/src/components/Settings/useAgentProfilesPanelState.ts b/frontend-modern/src/components/Settings/useAgentProfilesPanelState.ts index d5c28f15b..14468aa8e 100644 --- a/frontend-modern/src/components/Settings/useAgentProfilesPanelState.ts +++ b/frontend-modern/src/components/Settings/useAgentProfilesPanelState.ts @@ -1,4 +1,4 @@ -import { createEffect, createMemo, createSignal, onMount } from 'solid-js'; +import { createMemo, createSignal, onMount } from 'solid-js'; import { useWebSocket } from '@/contexts/appRuntime'; import { AgentProfilesAPI, @@ -26,7 +26,6 @@ import { getUpgradeActionButtonClass, UPGRADE_ACTION_LABEL, } from '@/utils/upgradePresentation'; -import { trackPaywallViewed, trackUpgradeClicked } from '@/utils/upgradeMetrics'; import { KNOWN_SETTINGS } from './agentProfileSettings'; import { getActionableAgentIdFromResource, @@ -205,14 +204,6 @@ export const useAgentProfilesPanelState = () => { } }; - createEffect((wasPaywallVisible) => { - const isPaywallVisible = !checkingLicense() && !hasAgentProfiles(); - if (showUpgradePrompts() && isPaywallVisible && !wasPaywallVisible) { - trackPaywallViewed('agent_profiles', 'settings_agent_profiles_panel'); - } - return isPaywallVisible; - }, false); - onMount(async () => { await loadRuntimeCapabilities(); @@ -372,7 +363,6 @@ export const useAgentProfilesPanelState = () => { showUpgradePrompts, showModal, showSuggestModal, - trackUpgradeClicked, unknownKeys, updateSetting, editingProfile, diff --git a/frontend-modern/src/components/Settings/useAuditLogPanelState.ts b/frontend-modern/src/components/Settings/useAuditLogPanelState.ts index d133d1146..8803a8215 100644 --- a/frontend-modern/src/components/Settings/useAuditLogPanelState.ts +++ b/frontend-modern/src/components/Settings/useAuditLogPanelState.ts @@ -11,7 +11,6 @@ import { hasFeature, runtimeCapabilitiesLoaded } from '@/stores/license'; import { getUpgradeActionDestination } from '@/stores/licenseCommercial'; import { presentationPolicyHidesUpgradePrompts } from '@/stores/sessionPresentationPolicy'; import { loadRuntimeCapabilities } from '@/stores/license'; -import { trackPaywallViewed, trackUpgradeClicked } from '@/utils/upgradeMetrics'; export interface AuditEvent { id: string; @@ -511,18 +510,6 @@ export const useAuditLogPanelState = () => { const goToNextPage = () => goToOffset(pageOffset() + pageSize()); const goToLastPage = () => goToOffset((totalPages() - 1) * pageSize()); - const handleUpgradeClick = () => { - trackUpgradeClicked('settings_audit_log_panel', 'audit_logging'); - }; - - createEffect((wasPaywallVisible) => { - const isPaywallVisible = showUpgradePaywall(); - if (showUpgradePrompts() && isPaywallVisible && !wasPaywallVisible) { - trackPaywallViewed('audit_logging', 'settings_audit_log_panel'); - } - return isPaywallVisible; - }, false); - onMount(() => { setIsMounted(true); void loadRuntimeCapabilities(); @@ -586,7 +573,6 @@ export const useAuditLogPanelState = () => { goToLastPage, goToNextPage, goToPreviousPage, - handleUpgradeClick, hasNextPage, hasResumeEvents, hasSignedEvents, diff --git a/frontend-modern/src/components/Settings/useAuditWebhookPanelState.ts b/frontend-modern/src/components/Settings/useAuditWebhookPanelState.ts index 903d8c180..14505e261 100644 --- a/frontend-modern/src/components/Settings/useAuditWebhookPanelState.ts +++ b/frontend-modern/src/components/Settings/useAuditWebhookPanelState.ts @@ -2,11 +2,10 @@ import { createEffect, createSignal, onMount } from 'solid-js'; import { apiFetchJSON } from '@/utils/apiClient'; import { logger } from '@/utils/logger'; import { showSuccess, showWarning } from '@/utils/toast'; -import { hasFeature, runtimeCapabilitiesLoaded } from '@/stores/license'; +import { hasFeature } from '@/stores/license'; import { getUpgradeActionDestination } from '@/stores/licenseCommercial'; import { presentationPolicyHidesUpgradePrompts } from '@/stores/sessionPresentationPolicy'; import { loadRuntimeCapabilities } from '@/stores/license'; -import { trackPaywallViewed } from '@/utils/upgradeMetrics'; import { getAuditWebhookDuplicateUrlMessage, getAuditWebhookInvalidUrlMessage, @@ -83,14 +82,6 @@ export const useAuditWebhookPanelState = (canManageOverride?: boolean) => { loadRuntimeCapabilities(); }); - createEffect((wasPaywallVisible: boolean) => { - const isPaywallVisible = runtimeCapabilitiesLoaded() && !hasFeature('audit_logging'); - if (showUpgradePrompts() && isPaywallVisible && !wasPaywallVisible) { - trackPaywallViewed('audit_logging', 'settings_audit_webhook_panel'); - } - return isPaywallVisible; - }, false); - createEffect(() => { if (hasFeature('audit_logging')) { void fetchWebhooks(); diff --git a/frontend-modern/src/components/Settings/useInfrastructureInstallState.tsx b/frontend-modern/src/components/Settings/useInfrastructureInstallState.tsx index 085d55f67..fe776f2cd 100644 --- a/frontend-modern/src/components/Settings/useInfrastructureInstallState.tsx +++ b/frontend-modern/src/components/Settings/useInfrastructureInstallState.tsx @@ -26,10 +26,6 @@ import { resolveInstallerBaseUrl, } from '@/utils/agentInstallCommand'; import { getUnifiedAgentClipboardCopyErrorMessage } from '@/utils/unifiedAgentInventoryPresentation'; -import { - trackAgentInstallProfileSelected, - trackAgentInstallTokenGenerated, -} from '@/utils/upgradeMetrics'; import { buildCommandsByPlatform, buildDefaultTokenName, @@ -39,7 +35,6 @@ import { type AgentPlatform, type InstallProfile, type SetupHandoffState, - UNIFIED_AGENT_TELEMETRY_SURFACE, } from './infrastructureOperationsModel'; export interface InfrastructureInstallStateOptions { @@ -256,7 +251,6 @@ Pulse prepares the first-host install token from setup so you can move straight setTokenName(''); setConfirmedNoToken(false); setSetupHandoffAutoTokenFailed(false); - trackAgentInstallTokenGenerated(UNIFIED_AGENT_TELEMETRY_SURFACE, source); if (options.notifySuccess) { notificationStore.success( 'Token generated with Agent config + reporting, Docker, and Kubernetes permissions.', @@ -435,7 +429,6 @@ Pulse prepares the first-host install token from setup so you can move straight const handleInstallProfileChange = (profile: InstallProfile) => { setInstallProfile(profile); - trackAgentInstallProfileSelected(UNIFIED_AGENT_TELEMETRY_SURFACE, profile); }; const commandSections = createMemo(() => { diff --git a/frontend-modern/src/components/Settings/useProLicensePanelState.ts b/frontend-modern/src/components/Settings/useProLicensePanelState.ts index 58687360d..cb31b5b82 100644 --- a/frontend-modern/src/components/Settings/useProLicensePanelState.ts +++ b/frontend-modern/src/components/Settings/useProLicensePanelState.ts @@ -49,7 +49,6 @@ import { SELF_HOSTED_PRO_BILLING_PURCHASE_QUERY_PARAM, SELF_HOSTED_PRO_BILLING_USAGE_HREF, SELF_HOSTED_PRO_BILLING_USAGE_ROUTE, - isSelfHostedPurchaseStartDestination, type SelfHostedBillingPlanIntent, type SelfHostedBillingSection, } from '@/utils/pricingHandoff'; @@ -65,7 +64,6 @@ import { getMonitoredSystemLimitUsageSummary, resolveMonitoredSystemCapacityStatus, } from '@/utils/monitoredSystemPresentation'; -import { trackCheckoutClicked, trackPricingViewed } from '@/utils/upgradeMetrics'; import { resolveUpgradeDestination, type UpgradeDestination } from '@/utils/upgradeNavigation'; import { SELF_HOSTED_PRO_BILLING_PRESENTATION } from './selfHostedBillingPresentation'; @@ -228,27 +226,6 @@ export function useProLicensePanelState() { navigate(SELF_HOSTED_PRO_BILLING_PLAN_ROUTE, { replace: true, scroll: false }); }); - let trackedPlanPricingView = false; - createEffect(() => { - const planVisible = - panelDataSettled() && - activeSection() === 'plan' && - getSelfHostedBillingPlanIntent(location.search) === - SELF_HOSTED_PRO_BILLING_PLAN_SELECTION_INTENT; - if (!planVisible) { - trackedPlanPricingView = false; - return; - } - if (trackedPlanPricingView) { - return; - } - trackPricingViewed( - 'settings_self_hosted_billing_plan', - SELF_HOSTED_PRO_BILLING_PLAN_SELECTION_INTENT, - ); - trackedPlanPricingView = true; - }); - const setActiveSection = (section: string) => { if (section !== 'plan' && section !== 'usage') { return; @@ -296,13 +273,6 @@ export function useProLicensePanelState() { }; }); - const handlePlanSelectionPromptClick = () => { - trackCheckoutClicked( - 'settings_self_hosted_billing_compare_prompt', - SELF_HOSTED_PRO_BILLING_PLAN_SELECTION_INTENT, - ); - }; - const statusPresentation = createMemo(() => getLicenseSubscriptionStatusPresentation(subscriptionState()), ); @@ -477,17 +447,6 @@ export function useProLicensePanelState() { } }); - const handlePurchaseActivationActionClick = () => { - const action = purchaseActivationAction(); - if (!action || !isSelfHostedPurchaseStartDestination(action.destination.href)) { - return; - } - trackCheckoutClicked( - 'settings_self_hosted_billing_purchase_return', - purchaseActivationIntent() ?? SELF_HOSTED_PRO_BILLING_PLAN_SELECTION_INTENT, - ); - }; - const commercialMigrationNotice = createMemo(() => getCommercialMigrationNotice(entitlements()?.commercial_migration), ); @@ -630,10 +589,8 @@ export function useProLicensePanelState() { loading, looksLikeLegacyLicenseKey, planSelectionPrompt, - handlePlanSelectionPromptClick, purchaseActivationNotice, purchaseActivationAction, - handlePurchaseActivationActionClick, setActiveSection, setLicenseKey, showUsageSection, diff --git a/frontend-modern/src/components/Settings/useRBACFeatureGateState.ts b/frontend-modern/src/components/Settings/useRBACFeatureGateState.ts index ad4dc2ff9..748daf7c6 100644 --- a/frontend-modern/src/components/Settings/useRBACFeatureGateState.ts +++ b/frontend-modern/src/components/Settings/useRBACFeatureGateState.ts @@ -1,9 +1,8 @@ -import { Accessor, createEffect, createMemo, onMount } from 'solid-js'; +import { Accessor, createMemo, onMount } from 'solid-js'; import { hasFeature, runtimeCapabilitiesLoaded } from '@/stores/license'; import { presentationPolicyHidesUpgradePrompts } from '@/stores/sessionPresentationPolicy'; import { loadRuntimeCapabilities } from '@/stores/license'; import { getRBACFeatureGateCopy, type RBACFeatureGateCopy } from '@/utils/rbacPresentation'; -import { trackPaywallViewed } from '@/utils/upgradeMetrics'; export type RBACFeatureGateKind = 'roles' | 'user-assignments'; export type RBACFeatureGateLocation = 'settings_roles_panel' | 'settings_user_assignments_panel'; @@ -29,14 +28,6 @@ export function useRBACFeatureGateState(options: UseRBACFeatureGateStateOptions) void loadRuntimeCapabilities(); }); - createEffect((wasPaywallVisible) => { - const isPaywallVisible = paywallVisible(); - if (showUpgradePrompts() && isPaywallVisible && !wasPaywallVisible) { - trackPaywallViewed('rbac', options.paywallLocation); - } - return isPaywallVisible; - }, false); - return { featureGateCopy, licenseReady, diff --git a/frontend-modern/src/components/Settings/useRelaySettingsPanelState.ts b/frontend-modern/src/components/Settings/useRelaySettingsPanelState.ts index 316bee683..124e31dc7 100644 --- a/frontend-modern/src/components/Settings/useRelaySettingsPanelState.ts +++ b/frontend-modern/src/components/Settings/useRelaySettingsPanelState.ts @@ -1,9 +1,8 @@ -import { createEffect, createMemo, createSignal, onCleanup, onMount } from 'solid-js'; -import { hasFeature, runtimeCapabilitiesLoaded } from '@/stores/license'; +import { createMemo, createSignal, onCleanup, onMount } from 'solid-js'; +import { hasFeature } from '@/stores/license'; import { getUpgradeActionDestination } from '@/stores/licenseCommercial'; import { presentationPolicyHidesUpgradePrompts } from '@/stores/sessionPresentationPolicy'; import { loadRuntimeCapabilities } from '@/stores/license'; -import { trackPaywallViewed } from '@/utils/upgradeMetrics'; import { showError, showSuccess } from '@/utils/toast'; import { RelayAPI, type RelayConfig, type RelayStatus } from '@/api/relay'; import { OnboardingAPI, type OnboardingQRResponse } from '@/api/onboarding'; @@ -41,14 +40,6 @@ export function useRelaySettingsPanelState(props: RelaySettingsPanelProps) { const statusErrorMessage = createMemo(() => getRelayStatusErrorMessage(status())); const canShowPairing = createMemo(() => Boolean(config()?.enabled && status()?.connected)); - createEffect((wasPaywallVisible: boolean) => { - const isPaywallVisible = runtimeCapabilitiesLoaded() && !relayEnabled(); - if (showUpgradePrompts() && isPaywallVisible && !wasPaywallVisible) { - trackPaywallViewed('relay', 'settings_relay_panel'); - } - return isPaywallVisible; - }, false); - let statusInterval: ReturnType | undefined; const resetPairingState = () => { diff --git a/frontend-modern/src/components/Settings/useReportingPanelState.ts b/frontend-modern/src/components/Settings/useReportingPanelState.ts index 5352f355f..9295b2a13 100644 --- a/frontend-modern/src/components/Settings/useReportingPanelState.ts +++ b/frontend-modern/src/components/Settings/useReportingPanelState.ts @@ -6,7 +6,6 @@ import { hasFeature, runtimeCapabilitiesLoaded } from '@/stores/license'; import { getUpgradeActionDestination } from '@/stores/licenseCommercial'; import { presentationPolicyHidesUpgradePrompts } from '@/stores/sessionPresentationPolicy'; import { loadRuntimeCapabilities } from '@/stores/license'; -import { trackPaywallViewed } from '@/utils/upgradeMetrics'; import { getReportingCatalogErrorMessage, getReportingGenerateErrorMessage, @@ -58,14 +57,6 @@ export const useReportingPanelState = () => { loadRuntimeCapabilities(); }); - createEffect((wasVisible: boolean) => { - const visible = isLocked(); - if (showUpgradePrompts() && visible && !wasVisible) { - trackPaywallViewed(reportingFeatureId(), 'settings_reporting_panel'); - } - return visible; - }, false); - const loadReportingCatalog = async () => { if (reportingCatalogLoading()) { return; diff --git a/frontend-modern/src/components/Settings/useSSOProvidersState.ts b/frontend-modern/src/components/Settings/useSSOProvidersState.ts index 4460017e3..52e4246ab 100644 --- a/frontend-modern/src/components/Settings/useSSOProvidersState.ts +++ b/frontend-modern/src/components/Settings/useSSOProvidersState.ts @@ -1,4 +1,4 @@ -import { createEffect, createMemo, createSignal, onMount } from 'solid-js'; +import { createMemo, createSignal, onMount } from 'solid-js'; import { createStore } from 'solid-js/store'; import { notificationStore } from '@/stores/notifications'; import { logger } from '@/utils/logger'; @@ -6,7 +6,6 @@ import { hasFeature, runtimeCapabilitiesLoaded } from '@/stores/license'; import { getUpgradeActionDestination } from '@/stores/licenseCommercial'; import { presentationPolicyHidesUpgradePrompts } from '@/stores/sessionPresentationPolicy'; import { loadRuntimeCapabilities } from '@/stores/license'; -import { trackPaywallViewed, trackUpgradeClicked } from '@/utils/upgradeMetrics'; import { getSSOCopySuccessMessage, getSSOConnectionTestErrorMessage, @@ -66,23 +65,6 @@ export const useSSOProvidersState = (props: SSOProvidersPanelProps) => { const canManage = () => props.canManage !== false; const showUpgradePrompts = () => !presentationPolicyHidesUpgradePrompts(); - createEffect((wasBannerVisible) => { - const isBannerVisible = - showUpgradePrompts() && runtimeCapabilitiesLoaded() && !hasAdvancedSSO() && !loading(); - if (isBannerVisible && !wasBannerVisible) { - trackPaywallViewed('advanced_sso', 'settings_sso_providers_banner'); - } - return isBannerVisible; - }, false); - - createEffect((wasUpsellVisible) => { - const isUpsellVisible = showUpgradePrompts() && showSamlUpsell(); - if (isUpsellVisible && !wasUpsellVisible) { - trackPaywallViewed('advanced_sso', 'settings_sso_providers_add_saml_gate'); - } - return isUpsellVisible; - }, false); - const loadProviders = async () => { setLoading(true); try { @@ -347,6 +329,5 @@ export const useSSOProvidersState = (props: SSOProvidersPanelProps) => { fetchMetadataPreview, getUpgradeActionDestination, runtimeCapabilitiesLoaded, - trackUpgradeClicked, }; }; diff --git a/frontend-modern/src/components/SetupWizard/SetupCompletionPanel.tsx b/frontend-modern/src/components/SetupWizard/SetupCompletionPanel.tsx index e7483a0e7..7697cb375 100644 --- a/frontend-modern/src/components/SetupWizard/SetupCompletionPanel.tsx +++ b/frontend-modern/src/components/SetupWizard/SetupCompletionPanel.tsx @@ -6,7 +6,6 @@ import { getPulseBaseUrl } from '@/utils/url'; import type { State } from '@/types/api'; import type { Resource } from '@/types/resource'; import { buildInfrastructureOnboardingPath } from '@/components/Settings/infrastructureWorkspaceModel'; -import { trackAgentFirstConnected } from '@/utils/upgradeMetrics'; import type { WizardState } from '../SetupWizard'; import { buildSetupCompletionConnectedSystems, @@ -36,7 +35,6 @@ const SOURCE_STRATEGY_OPTIONS = [ const ADD_INFRASTRUCTURE_PATH = buildInfrastructureOnboardingPath('pick'); const AGENT_INSTALL_PATH = buildInfrastructureOnboardingPath('agent'); -const SETUP_WIZARD_TELEMETRY_SURFACE = 'setup_wizard_complete'; export const SetupCompletionPanel: Component = (props) => { const [copied, setCopied] = createSignal<'password' | 'admin-token' | null>(null); @@ -44,7 +42,6 @@ export const SetupCompletionPanel: Component = (props) => { const [connectedSystems, setConnectedSystems] = createSignal< ReturnType >([]); - let firstAgentConnectionTracked = false; createEffect(() => { if (props.connectedResourcesOverride !== undefined) { @@ -81,14 +78,6 @@ export const SetupCompletionPanel: Component = (props) => { ); }); - if ( - !firstAgentConnectionTracked && - nextConnectedSystems.some((system) => system.connectionPath === 'agent') - ) { - trackAgentFirstConnected(SETUP_WIZARD_TELEMETRY_SURFACE, 'first_agent'); - firstAgentConnectionTracked = true; - } - if (hasConnectionChanges) { setConnectedSystems(nextConnectedSystems); } diff --git a/frontend-modern/src/components/SetupWizard/__tests__/SetupCompletionPanel.guardrails.test.ts b/frontend-modern/src/components/SetupWizard/__tests__/SetupCompletionPanel.guardrails.test.ts index 2a2c328d7..d363cddf4 100644 --- a/frontend-modern/src/components/SetupWizard/__tests__/SetupCompletionPanel.guardrails.test.ts +++ b/frontend-modern/src/components/SetupWizard/__tests__/SetupCompletionPanel.guardrails.test.ts @@ -31,6 +31,8 @@ describe('SetupCompletionPanel guardrails', () => { expect(setupCompletionPanelSource).not.toContain('Monitor from Anywhere'); expect(setupCompletionPanelSource).not.toContain("trackPaywallViewed('relay'"); expect(setupCompletionPanelSource).not.toContain("trackUpgradeClicked('setup_wizard'"); + expect(setupCompletionPanelSource).not.toContain('infrastructureOnboardingMetrics'); + expect(setupCompletionPanelSource).not.toContain('trackAgentFirstConnected'); expect(setupCompletionPanelSource).not.toContain('getUpgradeActionUrlOrFallback'); }); diff --git a/frontend-modern/src/components/SetupWizard/__tests__/SetupCompletionPanel.test.tsx b/frontend-modern/src/components/SetupWizard/__tests__/SetupCompletionPanel.test.tsx index 1a46dee7d..81dd8bc43 100644 --- a/frontend-modern/src/components/SetupWizard/__tests__/SetupCompletionPanel.test.tsx +++ b/frontend-modern/src/components/SetupWizard/__tests__/SetupCompletionPanel.test.tsx @@ -7,7 +7,6 @@ const copyToClipboardMock = vi.fn(); const apiFetchJSONMock = vi.fn(); const createObjectURLMock = vi.fn(() => 'blob:mock-url'); const revokeObjectURLMock = vi.fn(); -const trackAgentFirstConnectedMock = vi.fn(); class MockBlob { readonly parts: string[]; @@ -50,10 +49,6 @@ vi.mock('@/utils/logger', () => ({ }, })); -vi.mock('@/utils/upgradeMetrics', () => ({ - trackAgentFirstConnected: (...args: unknown[]) => trackAgentFirstConnectedMock(...args), -})); - const baseState: WizardState = { username: 'admin', password: 'password', @@ -213,10 +208,6 @@ describe('SetupCompletionPanel', () => { expect(screen.getAllByRole('button', { name: 'Open Infrastructure' }).length).toBeGreaterThan(0); expect(screen.getAllByRole('button', { name: 'Add infrastructure' }).length).toBeGreaterThan(0); expect(screen.queryByRole('button', { name: 'Install Pulse Agent' })).not.toBeInTheDocument(); - expect(trackAgentFirstConnectedMock).toHaveBeenCalledWith( - 'setup_wizard_complete', - 'first_agent', - ); const nextStepHeading = screen.getByRole('heading', { name: 'Open Infrastructure' }); const nextStepCard = nextStepHeading.closest('[aria-label="Setup next step"]'); @@ -271,7 +262,6 @@ describe('SetupCompletionPanel', () => { expect(screen.getByRole('button', { name: 'Open Infrastructure' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Add infrastructure' })).toBeInTheDocument(); expect(screen.queryByRole('button', { name: 'Install Pulse Agent' })).not.toBeInTheDocument(); - expect(trackAgentFirstConnectedMock).not.toHaveBeenCalled(); const nextStepHeading = screen.getByRole('heading', { name: 'Open Infrastructure' }); const nextStepCard = nextStepHeading.closest('[aria-label="Setup next step"]'); @@ -375,10 +365,6 @@ describe('SetupCompletionPanel', () => { ).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Add infrastructure' })).toBeInTheDocument(); expect(screen.queryByRole('button', { name: 'Install Pulse Agent' })).not.toBeInTheDocument(); - expect(trackAgentFirstConnectedMock).toHaveBeenCalledWith( - 'setup_wizard_complete', - 'first_agent', - ); }); it('keeps connected governed infrastructure on local operator identity', async () => { diff --git a/frontend-modern/src/components/shared/MonitoredSystemLimitWarningBanner.tsx b/frontend-modern/src/components/shared/MonitoredSystemLimitWarningBanner.tsx index 46ac53331..66f5f57f7 100644 --- a/frontend-modern/src/components/shared/MonitoredSystemLimitWarningBanner.tsx +++ b/frontend-modern/src/components/shared/MonitoredSystemLimitWarningBanner.tsx @@ -28,7 +28,6 @@ export const MonitoredSystemLimitWarningBanner: Component = () => { {MONITORED_SYSTEM_LIMIT_INSTALL_COLLECTORS_LABEL} diff --git a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts index 7ac1662e4..5ee38ed3a 100644 --- a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts +++ b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts @@ -897,7 +897,7 @@ describe('shared primitive guardrails', () => { expect(monitoredSystemLimitWarningBannerStateSource).toContain( 'export function useMonitoredSystemLimitWarningBannerState', ); - expect(monitoredSystemLimitWarningBannerStateSource).toContain('createEffect'); + expect(monitoredSystemLimitWarningBannerStateSource).not.toContain('createEffect'); expect(monitoredSystemLimitWarningBannerStateSource).toContain('createMemo'); expect(monitoredSystemLimitWarningBannerStateSource).toContain('loadRuntimeCapabilities'); expect(monitoredSystemLimitWarningBannerStateSource).toContain( @@ -910,7 +910,7 @@ describe('shared primitive guardrails', () => { expect(monitoredSystemLimitWarningBannerStateSource).toContain( 'presentationPolicyHidesUpgradePrompts', ); - expect(monitoredSystemLimitWarningBannerStateSource).toContain('trackUpgradeMetricEvent'); + expect(monitoredSystemLimitWarningBannerStateSource).not.toContain('trackUpgradeMetricEvent'); expect(monitoredSystemLimitWarningBannerStateSource).toContain('hasMigrationGap'); expect(monitoredSystemLimitWarningBannerStateSource).not.toContain( 'scopeSelfHostedBillingDestination', diff --git a/frontend-modern/src/components/shared/__tests__/MonitoredSystemLimitWarningBanner.test.tsx b/frontend-modern/src/components/shared/__tests__/MonitoredSystemLimitWarningBanner.test.tsx index 8742c5cac..73aa7ea5e 100644 --- a/frontend-modern/src/components/shared/__tests__/MonitoredSystemLimitWarningBanner.test.tsx +++ b/frontend-modern/src/components/shared/__tests__/MonitoredSystemLimitWarningBanner.test.tsx @@ -32,8 +32,6 @@ const mockLegacyConnections = vi.hoisted(() => kubernetes_clusters: 0, })), ); -const mockTrackUpgradeMetricEvent = vi.hoisted(() => vi.fn()); -const mockTrackUpgradeClicked = vi.hoisted(() => vi.fn()); const mockLoadRuntimeLicenseStatus = vi.hoisted(() => vi.fn()); const mockPresentationPolicyHidesCommercialSurfaces = vi.hoisted(() => vi.fn(() => false)); const mockPresentationPolicyHidesUpgradePrompts = vi.hoisted(() => vi.fn(() => false)); @@ -60,14 +58,6 @@ vi.mock('@/stores/sessionPresentationPolicy', () => ({ presentationPolicyHidesUpgradePrompts: () => mockPresentationPolicyHidesUpgradePrompts(), })); -vi.mock('@/utils/upgradeMetrics', () => ({ - UPGRADE_METRIC_EVENTS: { - LIMIT_WARNING_SHOWN: 'limit_warning_shown', - }, - trackUpgradeMetricEvent: mockTrackUpgradeMetricEvent, - trackUpgradeClicked: mockTrackUpgradeClicked, -})); - describe('MonitoredSystemLimitWarningBanner', () => { beforeEach(() => { localStorage.clear(); @@ -126,7 +116,7 @@ describe('MonitoredSystemLimitWarningBanner', () => { expect(monitoredSystemLimitWarningBannerStateSource).toContain( 'export function useMonitoredSystemLimitWarningBannerState', ); - expect(monitoredSystemLimitWarningBannerStateSource).toContain('createEffect'); + expect(monitoredSystemLimitWarningBannerStateSource).not.toContain('createEffect'); expect(monitoredSystemLimitWarningBannerStateSource).toContain('createMemo'); expect(monitoredSystemLimitWarningBannerStateSource).toContain('loadRuntimeCapabilities'); expect(monitoredSystemLimitWarningBannerStateSource).not.toContain('loadCommercialPosture'); @@ -137,7 +127,7 @@ describe('MonitoredSystemLimitWarningBanner', () => { expect(monitoredSystemLimitWarningBannerStateSource).toContain( 'presentationPolicyHidesUpgradePrompts', ); - expect(monitoredSystemLimitWarningBannerStateSource).toContain('trackUpgradeMetricEvent'); + expect(monitoredSystemLimitWarningBannerStateSource).not.toContain('trackUpgradeMetricEvent'); expect(monitoredSystemLimitWarningBannerStateSource).toContain('hasMigrationGap'); expect(monitoredSystemLimitWarningBannerStateSource).not.toContain( 'scopeSelfHostedBillingDestination', @@ -244,7 +234,6 @@ describe('MonitoredSystemLimitWarningBanner', () => { expect(screen.queryByText(/Monitored systems:/i)).not.toBeInTheDocument(); expect(screen.queryByText('Review options')).not.toBeInTheDocument(); - expect(mockTrackUpgradeMetricEvent).not.toHaveBeenCalled(); }); it('keeps urgent limit warnings visible with migration context', async () => { @@ -299,7 +288,6 @@ describe('MonitoredSystemLimitWarningBanner', () => { expect( screen.queryByText('5 monitored systems.'), ).not.toBeInTheDocument(); - expect(mockTrackUpgradeMetricEvent).not.toHaveBeenCalled(); }); it('stays hidden when self-hosted upgrade prompts are suppressed', async () => { @@ -322,7 +310,6 @@ describe('MonitoredSystemLimitWarningBanner', () => { expect( screen.queryByText('5 monitored systems.'), ).not.toBeInTheDocument(); - expect(mockTrackUpgradeMetricEvent).not.toHaveBeenCalled(); }); it('stays hidden for self-hosted installs even when stale continuity metadata is urgent', async () => { @@ -343,6 +330,5 @@ describe('MonitoredSystemLimitWarningBanner', () => { expect( screen.queryByText('5 monitored systems.'), ).not.toBeInTheDocument(); - expect(mockTrackUpgradeMetricEvent).not.toHaveBeenCalled(); }); }); diff --git a/frontend-modern/src/components/shared/useMonitoredSystemLimitWarningBannerState.ts b/frontend-modern/src/components/shared/useMonitoredSystemLimitWarningBannerState.ts index 8bd044c7c..d24a53c32 100644 --- a/frontend-modern/src/components/shared/useMonitoredSystemLimitWarningBannerState.ts +++ b/frontend-modern/src/components/shared/useMonitoredSystemLimitWarningBannerState.ts @@ -1,4 +1,4 @@ -import { createEffect, createMemo, onMount } from 'solid-js'; +import { createMemo, onMount } from 'solid-js'; import { presentationPolicyHidesCommercialSurfaces, presentationPolicyHidesUpgradePrompts, @@ -11,11 +11,6 @@ import { } from '@/stores/license'; import { hasMigrationGap } from '@/stores/licenseCommercial'; import { resolveUpgradeDestination } from '@/utils/upgradeNavigation'; -import { - trackUpgradeClicked, - trackUpgradeMetricEvent, - UPGRADE_METRIC_EVENTS, -} from '@/utils/upgradeMetrics'; import { getMonitoredSystemBannerToneClass, getMonitoredSystemSummary, @@ -54,32 +49,8 @@ export function useMonitoredSystemLimitWarningBannerState() { const installCollectorsDestination = createMemo(() => resolveUpgradeDestination(MONITORED_SYSTEM_LIMIT_INSTALL_COLLECTORS_HREF), ); - let wasUrgent = false; - createEffect(() => { - const urgent = isUrgent(); - const visible = showBanner(); - const limit = monitoredSystemLimit(); - if (visible && urgent && !wasUrgent && limit) { - trackUpgradeMetricEvent({ - type: UPGRADE_METRIC_EVENTS.LIMIT_WARNING_SHOWN, - surface: 'monitored_system_limit_banner', - limit_key: MONITORED_SYSTEM_LIMIT_KEY, - current_value: limit.current, - limit_value: limit.limit, - }); - } - wasUrgent = visible && urgent; - }); - - const handleInstallCollectorsClick = () => { - trackUpgradeClicked( - 'monitored_system_limit_banner_install_v6_collectors', - MONITORED_SYSTEM_LIMIT_KEY, - ); - }; return { - handleInstallCollectorsClick, installCollectorsDestination, isUrgent, reviewPolicyDestination, diff --git a/frontend-modern/src/pages/CloudPricing.tsx b/frontend-modern/src/pages/CloudPricing.tsx index f57dca7b1..34a408432 100644 --- a/frontend-modern/src/pages/CloudPricing.tsx +++ b/frontend-modern/src/pages/CloudPricing.tsx @@ -2,8 +2,6 @@ import { For, Show } from 'solid-js'; import { A } from '@solidjs/router'; import { Card } from '@/components/shared/Card'; import { PageHeader } from '@/components/shared/PageHeader'; -import { trackPaywallViewed } from '@/utils/upgradeMetrics'; -import { onMount } from 'solid-js'; import { CLOUD_COMMERCIAL_PRESENTATION, CLOUD_PLAN_DEFINITIONS, @@ -81,10 +79,6 @@ function CloudTierCard(props: { tier: CloudPlanDefinition }) { // --------------------------------------------------------------------------- export default function CloudPricing() { - onMount(() => { - trackPaywallViewed('cloud_pricing', 'cloud_pricing_page'); - }); - return (
{ - trackPaywallViewed('pricing', 'pricing_handoff'); if (externalDestination() || selfHostedPurchaseStartDestination()) { handoffToExternalPricing(destination()); } diff --git a/frontend-modern/src/pages/__tests__/AIIntelligence.test.tsx b/frontend-modern/src/pages/__tests__/AIIntelligence.test.tsx index 6beb0f60d..c8fbc918d 100644 --- a/frontend-modern/src/pages/__tests__/AIIntelligence.test.tsx +++ b/frontend-modern/src/pages/__tests__/AIIntelligence.test.tsx @@ -107,8 +107,6 @@ const loadLicenseStatusMock = vi.fn(); const loadCommercialPostureMock = vi.fn(); const getUpgradeActionDestinationMock = vi.fn(); const getUpgradeActionUrlOrFallbackMock = vi.fn(); -const trackPaywallViewedMock = vi.fn(); -const trackUpgradeClickedMock = vi.fn(); const presentationPolicyHidesUpgradePromptsMock = vi.fn(); const notificationSuccessMock = vi.fn(); const notificationErrorMock = vi.fn(); @@ -148,11 +146,6 @@ vi.mock('@/stores/licenseCommercial', () => ({ getUpgradeActionUrlOrFallback: (...args: unknown[]) => getUpgradeActionUrlOrFallbackMock(...args), })); -vi.mock('@/utils/upgradeMetrics', () => ({ - trackPaywallViewed: (...args: unknown[]) => trackPaywallViewedMock(...args), - trackUpgradeClicked: (...args: unknown[]) => trackUpgradeClickedMock(...args), -})); - vi.mock('@/stores/sessionPresentationPolicy', () => ({ presentationPolicyHidesUpgradePrompts: () => presentationPolicyHidesUpgradePromptsMock(), })); @@ -343,8 +336,6 @@ describe('AIIntelligence entitlement gating', () => { loadCommercialPostureMock.mockReset(); getUpgradeActionDestinationMock.mockReset(); getUpgradeActionUrlOrFallbackMock.mockReset(); - trackPaywallViewedMock.mockReset(); - trackUpgradeClickedMock.mockReset(); presentationPolicyHidesUpgradePromptsMock.mockReset(); notificationSuccessMock.mockReset(); notificationErrorMock.mockReset(); @@ -583,7 +574,6 @@ describe('AIIntelligence entitlement gating', () => { expect(screen.queryByRole('link', { name: 'Upgrade to Pro' })).not.toBeInTheDocument(); expect(screen.queryByRole('link', { name: 'Upgrade' })).not.toBeInTheDocument(); expect(screen.queryByRole('button', { name: /start free trial/i })).not.toBeInTheDocument(); - expect(trackPaywallViewedMock).not.toHaveBeenCalled(); }); it('locks paid patrol controls without upgrade prompts in default self-hosted mode', async () => { @@ -610,7 +600,6 @@ describe('AIIntelligence entitlement gating', () => { expect(screen.queryByRole('link', { name: 'Upgrade to Pro' })).not.toBeInTheDocument(); expect(screen.queryByRole('link', { name: 'Upgrade' })).not.toBeInTheDocument(); expect(screen.queryByRole('button', { name: /start free trial/i })).not.toBeInTheDocument(); - expect(trackPaywallViewedMock).not.toHaveBeenCalled(); }); it('unlocks paid patrol controls when the entitlement grants the features', async () => { @@ -633,7 +622,6 @@ describe('AIIntelligence entitlement gating', () => { expect(screen.getByRole('button', { name: 'Remediate' })).not.toBeDisabled(); expect(screen.queryByRole('link', { name: 'Upgrade to Pro' })).not.toBeInTheDocument(); expect(screen.queryByRole('link', { name: 'Upgrade' })).not.toBeInTheDocument(); - expect(trackPaywallViewedMock).not.toHaveBeenCalled(); }); it('renders the canonical intelligence summary card with recent changes', async () => { diff --git a/frontend-modern/src/pages/__tests__/Alerts.readOnly.test.tsx b/frontend-modern/src/pages/__tests__/Alerts.readOnly.test.tsx index b731acecd..a845f6a8d 100644 --- a/frontend-modern/src/pages/__tests__/Alerts.readOnly.test.tsx +++ b/frontend-modern/src/pages/__tests__/Alerts.readOnly.test.tsx @@ -85,10 +85,6 @@ vi.mock('@/utils/logger', () => ({ logger: { error: vi.fn() }, })); -vi.mock('@/utils/upgradeMetrics', () => ({ - trackPaywallViewed: vi.fn(), -})); - vi.mock('@/features/alerts/OverviewTab', () => ({ OverviewTab: overviewTabSpy, })); diff --git a/frontend-modern/src/pages/__tests__/CloudPricing.test.tsx b/frontend-modern/src/pages/__tests__/CloudPricing.test.tsx index ea98e4458..c5818f499 100644 --- a/frontend-modern/src/pages/__tests__/CloudPricing.test.tsx +++ b/frontend-modern/src/pages/__tests__/CloudPricing.test.tsx @@ -1,13 +1,9 @@ -import { afterEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, describe, expect, it } from 'vitest'; import { cleanup, render, screen } from '@solidjs/testing-library'; import { Router, Route } from '@solidjs/router'; import CloudPricing from '@/pages/CloudPricing'; -vi.mock('@/utils/upgradeMetrics', () => ({ - trackPaywallViewed: vi.fn(), -})); - describe('CloudPricing', () => { afterEach(() => { cleanup(); diff --git a/frontend-modern/src/pages/__tests__/PricingHandoff.test.tsx b/frontend-modern/src/pages/__tests__/PricingHandoff.test.tsx index d05c343f2..60289d488 100644 --- a/frontend-modern/src/pages/__tests__/PricingHandoff.test.tsx +++ b/frontend-modern/src/pages/__tests__/PricingHandoff.test.tsx @@ -5,13 +5,8 @@ import PricingHandoff from '@/pages/PricingHandoff'; import pricingHandoffSource from '@/pages/PricingHandoff.tsx?raw'; import { getSelfHostedPurchaseStartUrl } from '@/utils/pricingHandoff'; -const trackPaywallViewedMock = vi.fn(); const handoffToExternalPricingMock = vi.fn(); -vi.mock('@/utils/upgradeMetrics', () => ({ - trackPaywallViewed: (...args: unknown[]) => trackPaywallViewedMock(...args), -})); - vi.mock('@/utils/pricingHandoff', async () => { const actual = await vi.importActual('@/utils/pricingHandoff'); @@ -23,7 +18,6 @@ vi.mock('@/utils/pricingHandoff', async () => { describe('PricingHandoff', () => { beforeEach(() => { - trackPaywallViewedMock.mockReset(); handoffToExternalPricingMock.mockReset(); window.scrollTo = vi.fn(); }); diff --git a/frontend-modern/src/utils/__tests__/agentProfilesPresentation.test.ts b/frontend-modern/src/utils/__tests__/agentProfilesPresentation.test.ts index df38c4dbc..5bbe15146 100644 --- a/frontend-modern/src/utils/__tests__/agentProfilesPresentation.test.ts +++ b/frontend-modern/src/utils/__tests__/agentProfilesPresentation.test.ts @@ -1,10 +1,19 @@ import { describe, expect, it } from 'vitest'; import { getAgentProfileAssignmentsEmptyState, + getAgentProfilesFeatureGateCopy, getAgentProfilesEmptyState, } from '@/utils/agentProfilesPresentation'; describe('agentProfilesPresentation', () => { + it('returns capability-focused feature gate copy', () => { + expect(getAgentProfilesFeatureGateCopy()).toMatchObject({ + title: 'Agent Profiles', + subtitle: 'Centralized agent configuration', + body: expect.not.stringContaining('Pro'), + }); + }); + it('returns canonical agent profile empty-state copy', () => { expect(getAgentProfilesEmptyState()).toBe('No profiles yet. Create one to get started.'); expect(getAgentProfileAssignmentsEmptyState()).toBe( diff --git a/frontend-modern/src/utils/__tests__/aiControlLevelPresentation.test.ts b/frontend-modern/src/utils/__tests__/aiControlLevelPresentation.test.ts index 6a1101c37..c3fe9aa38 100644 --- a/frontend-modern/src/utils/__tests__/aiControlLevelPresentation.test.ts +++ b/frontend-modern/src/utils/__tests__/aiControlLevelPresentation.test.ts @@ -40,7 +40,7 @@ describe('aiControlLevelPresentation', () => { }); expect(getAIChatControlLevelPresentation('autonomous')).toMatchObject({ label: 'Autonomous', - description: 'Executes without approval (Pro)', + description: 'Executes without approval', dotClassName: 'bg-red-500', }); }); diff --git a/frontend-modern/src/utils/__tests__/auditWebhookPresentation.test.ts b/frontend-modern/src/utils/__tests__/auditWebhookPresentation.test.ts index a0a73ceaa..c1ed5b3e3 100644 --- a/frontend-modern/src/utils/__tests__/auditWebhookPresentation.test.ts +++ b/frontend-modern/src/utils/__tests__/auditWebhookPresentation.test.ts @@ -11,9 +11,10 @@ import { describe('auditWebhookPresentation', () => { it('returns canonical feature gate copy', () => { expect(getAuditWebhookFeatureGateCopy()).toMatchObject({ - title: 'Audit Webhooks (Pro)', - body: expect.stringContaining('require Pro'), + title: 'Audit Webhooks', + body: expect.stringContaining('paid self-hosted and hosted plans'), }); + expect(getAuditWebhookFeatureGateCopy().body).not.toContain('Pro'); }); it('returns neutral feature gate copy when commercial prompts are hidden', () => { diff --git a/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts b/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts index b69f65fdd..5e061a84a 100644 --- a/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts +++ b/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts @@ -1650,8 +1650,13 @@ describe('frontend resource type boundaries', () => { expect(userAssignmentsPanelSource).not.toContain('Users sync on first login'); expect(rbacFeatureGateStateSource).toContain('getRBACFeatureGateCopy'); expect(rbacPresentationSource).toContain('export function getRBACFeatureGateCopy'); + expect(rbacPresentationSource).not.toContain('(Pro)'); expect(rbacPresentationSource).toContain('export function getRolesEmptyState'); expect(rbacPresentationSource).toContain('export function getUserAssignmentsEmptyStateCopy'); + expect(agentProfilesPanelSource).not.toContain('Pro feature'); + expect(agentProfilesPresentationSource).toContain( + 'export function getAgentProfilesFeatureGateCopy', + ); expect(agentProfilesPresentationSource).toContain('export function getAgentProfilesEmptyState'); expect(agentProfilesPresentationSource).toContain( 'export function getAgentProfileAssignmentsEmptyState', @@ -2933,10 +2938,10 @@ describe('frontend resource type boundaries', () => { expect(monitoredSystemLimitWarningBannerSource).not.toContain('loadRuntimeCapabilities'); expect(monitoredSystemLimitWarningBannerSource).not.toContain('trackUpgradeMetricEvent'); expect(monitoredSystemLimitWarningBannerSource).not.toContain('legacyConnections()'); - expect(monitoredSystemLimitWarningBannerStateSource).toContain('createEffect'); + expect(monitoredSystemLimitWarningBannerStateSource).not.toContain('createEffect'); expect(monitoredSystemLimitWarningBannerStateSource).toContain('createMemo'); expect(monitoredSystemLimitWarningBannerStateSource).toContain('loadRuntimeCapabilities'); - expect(monitoredSystemLimitWarningBannerStateSource).toContain('trackUpgradeMetricEvent'); + expect(monitoredSystemLimitWarningBannerStateSource).not.toContain('trackUpgradeMetricEvent'); expect(monitoredSystemLimitWarningBannerStateSource).toContain('hasMigrationGap'); expect(monitoredSystemLimitWarningBannerStateSource).toContain('reviewPolicyDestination'); expect(monitoredSystemLimitWarningBannerStateSource).not.toContain('handleUpgradeClick'); @@ -4130,6 +4135,8 @@ describe('frontend resource type boundaries', () => { expect(ssoProvidersPanelSource).not.toContain('const loadProviders = async () =>'); expect(ssoProvidersPanelSource).not.toContain('const handleSave = async ('); expect(ssoProvidersPanelSource).not.toContain('const testConnection = async () =>'); + expect(ssoProvidersPanelSource).not.toContain('Pro feature'); + expect(ssoProvidersPanelSource).not.toContain('requires Pro'); expect(ssoProvidersPanelSource).not.toContain('provider.type.toUpperCase()'); expect(ssoProvidersPanelSource).not.toContain("provider.type === 'oidc' ? ("); expect(ssoProviderPresentationSource).toContain('export function getSSOProviderTypeLabel'); @@ -4143,6 +4150,8 @@ describe('frontend resource type boundaries', () => { expect(ssoProviderPresentationSource).toContain( 'export function getSSOProviderEmptyStateDescription', ); + expect(ssoProviderPresentationSource).toContain('export function getSSOSamlFeatureGateCopy'); + expect(ssoProviderPresentationSource).not.toContain('(Pro)'); expect(ssoProviderPresentationSource).toContain('export function getSSOProvidersLoadingState'); expect(ssoProviderPresentationSource).toContain('export function getSSOTestResultPresentation'); expect(ssoProviderPresentationSource).toContain( @@ -4194,6 +4203,8 @@ describe('frontend resource type boundaries', () => { expect(auditWebhookPresentationSource).toContain( 'export function getAuditWebhookFeatureGateCopy', ); + expect(auditWebhookPresentationSource).not.toContain('Audit Webhooks (Pro)'); + expect(auditWebhookPresentationSource).not.toContain('require Pro'); expect(auditWebhookPresentationSource).toContain( 'export function getAuditWebhookEmptyStateCopy', ); @@ -4214,7 +4225,7 @@ describe('frontend resource type boundaries', () => { expect(auditWebhookPresentationSource).toContain('AUDIT_WEBHOOK_SECURITY_NOTE_BODY'); expect(auditWebhookStateSource).toContain('export const useAuditWebhookPanelState ='); expect(auditWebhookStateSource).toContain('loadRuntimeCapabilities();'); - expect(auditWebhookStateSource).toContain('trackPaywallViewed'); + expect(auditWebhookStateSource).not.toContain('trackPaywallViewed'); expect(auditWebhookStateSource).toContain('const fetchWebhooks = async () =>'); expect(auditWebhookStateSource).toContain('const saveWebhooks = async (urls: string[]) =>'); expect(auditLogPanelSource).toContain('getAuditLogLoadingState'); @@ -4231,7 +4242,7 @@ describe('frontend resource type boundaries', () => { expect(auditLogStateSource).toContain('createLocalStorageStringSignal'); expect(auditLogStateSource).toContain('const fetchAuditEvents = async ('); expect(auditLogStateSource).toContain('const verifyAllEvents = async ('); - expect(auditLogStateSource).toContain('trackPaywallViewed'); + expect(auditLogStateSource).not.toContain('trackPaywallViewed'); expect(auditLogPanelSource).toContain('getAuditEventTypeBadgeClass'); expect(auditLogPanelSource).toContain('getAuditVerificationBadgePresentation'); expect(auditLogPanelSource).toContain('getAuditEventStatusPresentation'); diff --git a/frontend-modern/src/utils/__tests__/infrastructureOnboardingMetrics.test.ts b/frontend-modern/src/utils/__tests__/infrastructureOnboardingMetrics.test.ts deleted file mode 100644 index 1f8b9101c..000000000 --- a/frontend-modern/src/utils/__tests__/infrastructureOnboardingMetrics.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { - clearSharedInfrastructureOnboardingMetricsTracker, - createInfrastructureOnboardingMetricsTracker, - getSharedInfrastructureOnboardingMetricsTracker, -} from '@/utils/infrastructureOnboardingMetrics'; -import infrastructureOnboardingMetricsSource from '@/utils/infrastructureOnboardingMetrics.ts?raw'; - -describe('infrastructureOnboardingMetrics', () => { - it('does not bridge infrastructure onboarding to maintainer analytics', () => { - expect(infrastructureOnboardingMetricsSource).not.toContain('trackUpgradeMetricEvent'); - expect(infrastructureOnboardingMetricsSource).not.toContain('UPGRADE_METRIC_EVENTS'); - expect(infrastructureOnboardingMetricsSource).not.toContain('/api/upgrade-metrics/events'); - expect(infrastructureOnboardingMetricsSource).not.toContain('sessionStorage'); - }); - - it('keeps the tracker contract callable as a compatibility no-op', () => { - const tracker = createInfrastructureOnboardingMetricsTracker(); - - tracker.recordOpened(); - tracker.recordOpened(); - tracker.recordPathSelected('api'); - tracker.recordPathSelected('api'); - tracker.recordProbeResult('no-match'); - tracker.recordProbeResult('error'); - tracker.recordCatalogSelected('truenas'); - tracker.recordCatalogSelected('truenas'); - tracker.recordCredentialsOpened('truenas'); - tracker.recordCredentialsOpened('truenas'); - }); - - it('returns the same no-op tracker for created and shared flows', () => { - const firstTracker = createInfrastructureOnboardingMetricsTracker(); - const secondTracker = getSharedInfrastructureOnboardingMetricsTracker(); - - expect(firstTracker).toBe(secondTracker); - - clearSharedInfrastructureOnboardingMetricsTracker(); - const thirdTracker = getSharedInfrastructureOnboardingMetricsTracker(); - - expect(thirdTracker).toBe(firstTracker); - }); -}); diff --git a/frontend-modern/src/utils/__tests__/rbacPresentation.test.ts b/frontend-modern/src/utils/__tests__/rbacPresentation.test.ts index 40e7e4037..6b136c92d 100644 --- a/frontend-modern/src/utils/__tests__/rbacPresentation.test.ts +++ b/frontend-modern/src/utils/__tests__/rbacPresentation.test.ts @@ -14,10 +14,12 @@ import { describe('rbacPresentation', () => { it('returns canonical feature gate copy', () => { expect(getRBACFeatureGateCopy('roles')).toMatchObject({ - title: 'Custom Roles (Pro)', + title: 'Custom Roles', + body: expect.stringContaining('paid self-hosted and hosted plans'), }); expect(getRBACFeatureGateCopy('user-assignments')).toMatchObject({ - title: 'Centralized Access Control (Pro)', + title: 'Centralized Access Control', + body: expect.stringContaining('paid self-hosted and hosted plans'), }); }); diff --git a/frontend-modern/src/utils/__tests__/ssoProviderPresentation.test.ts b/frontend-modern/src/utils/__tests__/ssoProviderPresentation.test.ts index 1f1bc0097..301823524 100644 --- a/frontend-modern/src/utils/__tests__/ssoProviderPresentation.test.ts +++ b/frontend-modern/src/utils/__tests__/ssoProviderPresentation.test.ts @@ -24,6 +24,7 @@ import { getSSOProviderToggleSuccessMessage, getSSOProviderTypeBadgeClass, getSSOProviderTypeLabel, + getSSOSamlFeatureGateCopy, getSSOTestResultPresentation, } from '../ssoProviderPresentation'; @@ -32,7 +33,7 @@ describe('ssoProviderPresentation', () => { expect(getSSOProviderTypeLabel('oidc')).toBe('OIDC'); expect(getSSOProviderTypeLabel('saml')).toBe('SAML'); expect(getSSOProviderAddButtonLabel('oidc')).toBe('Add OIDC'); - expect(getSSOProviderAddButtonLabel('saml', true)).toBe('Add SAML (Pro)'); + expect(getSSOProviderAddButtonLabel('saml')).toBe('Add SAML'); expect(getSSOProviderModalTitle(false, 'saml')).toBe('Add SAML Provider'); expect(getSSOProviderEmptyStateTitle()).toBe('No SSO providers configured'); expect(getSSOProviderEmptyStateDescription()).toBe( @@ -55,6 +56,15 @@ describe('ssoProviderPresentation', () => { ).toBe('https://idp.example.com/entity'); }); + it('returns capability-focused SAML feature gate copy', () => { + expect(getSSOSamlFeatureGateCopy()).toMatchObject({ + title: 'Advanced SSO', + subtitle: 'Advanced SSO', + body: expect.stringContaining('paid self-hosted and hosted plans'), + }); + expect(getSSOSamlFeatureGateCopy().body).not.toContain('Pro'); + }); + it('formats provider card and test result tones canonically', () => { expect(getSSOProviderCardClass(true)).toContain('bg-surface'); expect(getSSOProviderCardClass(false)).toContain('opacity-60'); diff --git a/frontend-modern/src/utils/__tests__/upgradeMetrics.test.ts b/frontend-modern/src/utils/__tests__/upgradeMetrics.test.ts deleted file mode 100644 index 6c35a4c87..000000000 --- a/frontend-modern/src/utils/__tests__/upgradeMetrics.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { - trackAgentFirstConnected, - trackAgentInstallCommandCopied, - trackAgentInstallProfileSelected, - trackAgentInstallTokenGenerated, - trackCheckoutClicked, - trackPaywallViewed, - trackPricingViewed, - trackUpgradeClicked, - trackUpgradeMetricEvent, -} from '@/utils/upgradeMetrics'; -import upgradeMetricsSource from '@/utils/upgradeMetrics.ts?raw'; - -describe('upgradeMetrics customer frontend boundary', () => { - it('does not carry browser-side ingestion plumbing for maintainer analytics', () => { - expect(upgradeMetricsSource).not.toContain('/api/upgrade-metrics/events'); - expect(upgradeMetricsSource).not.toContain('@/utils/apiClient'); - expect(upgradeMetricsSource).not.toContain('apiFetch('); - expect(upgradeMetricsSource).not.toContain('fetch('); - expect(upgradeMetricsSource).not.toContain('sendBeacon'); - }); - - it('keeps compatibility wrappers callable without emitting product analytics', () => { - trackPaywallViewed('rbac', 'settings_roles_panel'); - trackPricingViewed('settings_self_hosted_billing_plan', 'self_hosted_plan'); - trackUpgradeClicked('settings_reporting_panel', 'reporting'); - trackCheckoutClicked('settings_self_hosted_billing_compare_prompt', 'self_hosted_plan'); - trackAgentInstallTokenGenerated('settings_unified_agents', 'manual'); - trackAgentInstallCommandCopied('settings_unified_agents', 'linux:auto:install'); - trackAgentInstallProfileSelected('settings_unified_agents', 'proxmox-pbs'); - trackAgentFirstConnected('setup_wizard_complete', 'first_agent'); - trackUpgradeMetricEvent({ - type: 'agent_install_command_copied', - surface: 'settings_unified_agents', - capability: 'linux:auto:install:custom', - idempotencyKey: 'custom-1', - }); - }); -}); diff --git a/frontend-modern/src/utils/agentProfilesPresentation.ts b/frontend-modern/src/utils/agentProfilesPresentation.ts index b67153eb8..a56edddc9 100644 --- a/frontend-modern/src/utils/agentProfilesPresentation.ts +++ b/frontend-modern/src/utils/agentProfilesPresentation.ts @@ -1,3 +1,17 @@ +export interface AgentProfilesFeatureGateCopy { + title: string; + subtitle: string; + body: string; +} + +export function getAgentProfilesFeatureGateCopy(): AgentProfilesFeatureGateCopy { + return { + title: 'Agent Profiles', + subtitle: 'Centralized agent configuration', + body: 'Create reusable configuration profiles for your agents. Manage Docker monitoring, logging, and reporting intervals from a central location.', + }; +} + export function getAgentProfilesEmptyState(): string { return 'No profiles yet. Create one to get started.'; } diff --git a/frontend-modern/src/utils/aiControlLevelPresentation.ts b/frontend-modern/src/utils/aiControlLevelPresentation.ts index 3a18c5dec..611ed0941 100644 --- a/frontend-modern/src/utils/aiControlLevelPresentation.ts +++ b/frontend-modern/src/utils/aiControlLevelPresentation.ts @@ -53,7 +53,7 @@ export function getAIChatControlLevelPresentation( case 'autonomous': return { label: 'Autonomous', - description: 'Executes without approval (Pro)', + description: 'Executes without approval', pillClassName: 'border-red-200 text-red-700 bg-red-50 dark:border-red-800 dark:text-red-200 dark:bg-red-900', dotClassName: 'bg-red-500', diff --git a/frontend-modern/src/utils/auditWebhookPresentation.ts b/frontend-modern/src/utils/auditWebhookPresentation.ts index 574497dbc..9abf1fa3d 100644 --- a/frontend-modern/src/utils/auditWebhookPresentation.ts +++ b/frontend-modern/src/utils/auditWebhookPresentation.ts @@ -38,8 +38,8 @@ export function getAuditWebhookFeatureGateCopy( } return { - title: 'Audit Webhooks (Pro)', - body: 'Audit webhooks are part of the audit logging feature set and require Pro.', + title: 'Audit Webhooks', + body: 'Audit webhook delivery is available on paid self-hosted and hosted plans.', }; } diff --git a/frontend-modern/src/utils/conversionEvents.ts b/frontend-modern/src/utils/conversionEvents.ts deleted file mode 100644 index 8b8cb9582..000000000 --- a/frontend-modern/src/utils/conversionEvents.ts +++ /dev/null @@ -1,14 +0,0 @@ -// Compatibility wrapper for retired customer-side commercial analytics exports. -// Keep these stable while older modules/tests migrate away from the names. - -export type { UpgradeMetricEvent as ConversionEvent } from './upgradeMetrics'; -export { - UPGRADE_METRIC_EVENTS as CONVERSION_EVENTS, - trackUpgradeMetricEvent as trackConversionEvent, - trackPaywallViewed, - trackUpgradeClicked, - trackAgentInstallTokenGenerated, - trackAgentInstallCommandCopied, - trackAgentInstallProfileSelected, - trackAgentFirstConnected, -} from './upgradeMetrics'; diff --git a/frontend-modern/src/utils/infrastructureOnboardingMetrics.ts b/frontend-modern/src/utils/infrastructureOnboardingMetrics.ts deleted file mode 100644 index d94cb9ef6..000000000 --- a/frontend-modern/src/utils/infrastructureOnboardingMetrics.ts +++ /dev/null @@ -1,51 +0,0 @@ -import type { ConnectionType } from '@/api/connections'; - -export type InfrastructureOnboardingPath = 'api' | 'agent'; -export type InfrastructureOnboardingProbeOutcome = 'detected' | 'no-match' | 'error'; - -export const INFRASTRUCTURE_ONBOARDING_METRICS_SURFACE = 'settings_infrastructure_add'; - -export interface InfrastructureOnboardingMetricsTracker { - recordOpened: () => void; - recordPathSelected: (path: InfrastructureOnboardingPath) => void; - recordProbeResult: (outcome: InfrastructureOnboardingProbeOutcome) => void; - recordCatalogSelected: (type: ConnectionType) => void; - recordCredentialsOpened: (type: ConnectionType) => void; -} - -const NOOP_INFRASTRUCTURE_ONBOARDING_METRICS_TRACKER: InfrastructureOnboardingMetricsTracker = { - recordOpened() { - // Compatibility no-op. - }, - recordPathSelected(_path) { - // Compatibility no-op. - }, - recordProbeResult(_outcome) { - // Compatibility no-op. - }, - recordCatalogSelected(_type) { - // Compatibility no-op. - }, - recordCredentialsOpened(_type) { - // Compatibility no-op. - }, -}; - -export function createInfrastructureOnboardingMetricsTracker( - _surface = INFRASTRUCTURE_ONBOARDING_METRICS_SURFACE, - _flowId?: string, -): InfrastructureOnboardingMetricsTracker { - return NOOP_INFRASTRUCTURE_ONBOARDING_METRICS_TRACKER; -} - -export function getSharedInfrastructureOnboardingMetricsTracker( - _surface = INFRASTRUCTURE_ONBOARDING_METRICS_SURFACE, -): InfrastructureOnboardingMetricsTracker { - return NOOP_INFRASTRUCTURE_ONBOARDING_METRICS_TRACKER; -} - -export function clearSharedInfrastructureOnboardingMetricsTracker( - _surface = INFRASTRUCTURE_ONBOARDING_METRICS_SURFACE, -): void { - // Compatibility no-op. -} diff --git a/frontend-modern/src/utils/rbacPresentation.ts b/frontend-modern/src/utils/rbacPresentation.ts index 4f5cb17bd..e26bf0c8f 100644 --- a/frontend-modern/src/utils/rbacPresentation.ts +++ b/frontend-modern/src/utils/rbacPresentation.ts @@ -21,19 +21,17 @@ export function getRBACFeatureGateCopy( const showCommercialCopy = options.showCommercialCopy !== false; if (kind === 'roles') { return { - title: showCommercialCopy ? 'Custom Roles (Pro)' : 'Custom Roles', + title: 'Custom Roles', body: showCommercialCopy - ? 'Define granular permissions and custom access tiers for your team.' + ? 'Define granular permissions and custom access tiers on paid self-hosted and hosted plans.' : 'Define granular permissions and custom access tiers when RBAC is enabled for this instance.', }; } return { - title: showCommercialCopy - ? 'Centralized Access Control (Pro)' - : 'Centralized Access Control', + title: 'Centralized Access Control', body: showCommercialCopy - ? 'Assign multi-tier roles to users and manage infrastructure-wide security policies.' + ? 'Assign multi-tier roles to users and manage infrastructure-wide security policies on paid self-hosted and hosted plans.' : 'Assign roles to users and review access policy when RBAC is enabled for this instance.', }; } diff --git a/frontend-modern/src/utils/ssoProviderPresentation.ts b/frontend-modern/src/utils/ssoProviderPresentation.ts index 61b8aee58..26f062dc4 100644 --- a/frontend-modern/src/utils/ssoProviderPresentation.ts +++ b/frontend-modern/src/utils/ssoProviderPresentation.ts @@ -1,5 +1,11 @@ export type SSOProviderType = 'oidc' | 'saml'; +export interface SSOProviderFeatureGateCopy { + title: string; + subtitle: string; + body: string; +} + export interface SSOProviderSummaryLike { type: SSOProviderType; oidcIssuerUrl?: string; @@ -11,10 +17,16 @@ export function getSSOProviderTypeLabel(type: SSOProviderType): string { return type === 'oidc' ? 'OIDC' : 'SAML'; } -export function getSSOProviderAddButtonLabel(type: SSOProviderType, gated = false): string { - return gated - ? `Add ${getSSOProviderTypeLabel(type)} (Pro)` - : `Add ${getSSOProviderTypeLabel(type)}`; +export function getSSOProviderAddButtonLabel(type: SSOProviderType): string { + return `Add ${getSSOProviderTypeLabel(type)}`; +} + +export function getSSOSamlFeatureGateCopy(): SSOProviderFeatureGateCopy { + return { + title: 'Advanced SSO', + subtitle: 'Advanced SSO', + body: 'SAML 2.0 and multi-provider SSO are available on paid self-hosted and hosted plans. Basic OIDC remains available without a paid plan.', + }; } export function getSSOProviderModalTitle(editing: boolean, type: SSOProviderType): string { diff --git a/frontend-modern/src/utils/upgradeMetrics.ts b/frontend-modern/src/utils/upgradeMetrics.ts deleted file mode 100644 index 16ec24aa0..000000000 --- a/frontend-modern/src/utils/upgradeMetrics.ts +++ /dev/null @@ -1,81 +0,0 @@ -export interface UpgradeMetricEvent { - type: string; - capability?: string; - surface: string; - tenant_mode?: string; - limit_key?: string; - current_value?: number; - limit_value?: number; - timestamp: number; - idempotency_key: string; -} - -export interface TrackUpgradeMetricEventInput { - type: string; - surface: string; - capability?: string; - tenant_mode?: string; - limit_key?: string; - current_value?: number; - limit_value?: number; - idempotencyKey?: string; -} - -export const UPGRADE_METRIC_EVENTS = { - PRICING_VIEWED: 'pricing_viewed', - PAYWALL_VIEWED: 'paywall_viewed', - TRIAL_STARTED: 'trial_started', - LICENSE_ACTIVATED: 'license_activated', - UPGRADE_CLICKED: 'upgrade_clicked', - CHECKOUT_CLICKED: 'checkout_clicked', - LIMIT_WARNING_SHOWN: 'limit_warning_shown', - LIMIT_BLOCKED: 'limit_blocked', - AGENT_INSTALL_TOKEN_GENERATED: 'agent_install_token_generated', - AGENT_INSTALL_COMMAND_COPIED: 'agent_install_command_copied', - AGENT_INSTALL_PROFILE_SELECTED: 'agent_install_profile_selected', - AGENT_FIRST_CONNECTED: 'agent_first_connected', - INFRASTRUCTURE_ONBOARDING_OPENED: 'infrastructure_onboarding_opened', - INFRASTRUCTURE_ONBOARDING_PATH_SELECTED: 'infrastructure_onboarding_path_selected', - INFRASTRUCTURE_ONBOARDING_PROBE_RESULT: 'infrastructure_onboarding_probe_result', - INFRASTRUCTURE_ONBOARDING_CATALOG_SELECTED: 'infrastructure_onboarding_catalog_selected', - INFRASTRUCTURE_ONBOARDING_CREDENTIALS_OPENED: 'infrastructure_onboarding_credentials_opened', -} as const; - -export function trackUpgradeMetricEvent( - _event: TrackUpgradeMetricEventInput, -): void { - // Compatibility no-op: customer frontend surfaces must not emit maintainer - // commercial, funnel, or onboarding analytics. -} - -export function trackPaywallViewed(_capability: string, _surface: string): void { - // Compatibility no-op. -} - -export function trackPricingViewed(_surface: string, _capability?: string): void { - // Compatibility no-op. -} - -export function trackUpgradeClicked(_surface: string, _capability?: string): void { - // Compatibility no-op. -} - -export function trackCheckoutClicked(_surface: string, _capability?: string): void { - // Compatibility no-op. -} - -export function trackAgentInstallTokenGenerated(_surface: string, _capability?: string): void { - // Compatibility no-op. -} - -export function trackAgentInstallCommandCopied(_surface: string, _capability?: string): void { - // Compatibility no-op. -} - -export function trackAgentInstallProfileSelected(_surface: string, _profile: string): void { - // Compatibility no-op. -} - -export function trackAgentFirstConnected(_surface: string, _capability?: string): void { - // Compatibility no-op. -} diff --git a/internal/api/contract_test.go b/internal/api/contract_test.go index cacb06fad..75efa0fbc 100644 --- a/internal/api/contract_test.go +++ b/internal/api/contract_test.go @@ -2497,8 +2497,8 @@ func TestContract_ReportingCatalogJSONSnapshot(t *testing.T) { "title":"Detailed Reporting", "description":"Generate performance reports and current-state exports across infrastructure and workloads.", "lockedState":{ - "title":"Advanced Reporting (Pro)", - "description":"Generate PDF and CSV performance reports plus current-state VM inventory exports across infrastructure and workload resources." + "title":"Advanced Reporting", + "description":"Generate PDF and CSV performance reports plus current-state VM inventory exports across infrastructure and workload resources on paid self-hosted and hosted plans." }, "guidance":{ "title":"Advanced Insights", diff --git a/pkg/reporting/catalog.go b/pkg/reporting/catalog.go index fd3f55425..33d110597 100644 --- a/pkg/reporting/catalog.go +++ b/pkg/reporting/catalog.go @@ -180,8 +180,8 @@ func DescribeReportingCatalog() ReportingCatalog { Title: "Detailed Reporting", Description: "Generate performance reports and current-state exports across infrastructure and workloads.", LockedState: ReportingLockedStateDefinition{ - Title: "Advanced Reporting (Pro)", - Description: "Generate PDF and CSV performance reports plus current-state VM inventory exports across infrastructure and workload resources.", + Title: "Advanced Reporting", + Description: "Generate PDF and CSV performance reports plus current-state VM inventory exports across infrastructure and workload resources on paid self-hosted and hosted plans.", }, Guidance: ReportingGuidanceDefinition{ Title: "Advanced Insights", diff --git a/pkg/reporting/catalog_test.go b/pkg/reporting/catalog_test.go index e0e41dbfa..1460a0913 100644 --- a/pkg/reporting/catalog_test.go +++ b/pkg/reporting/catalog_test.go @@ -14,8 +14,8 @@ func TestDescribeReportingCatalog_DefinesCanonicalSurfaces(t *testing.T) { if catalog.PerformanceReport.ID != "performance_reports" { t.Fatalf("performance report ID = %q, want performance_reports", catalog.PerformanceReport.ID) } - if catalog.LockedState.Title != "Advanced Reporting (Pro)" { - t.Fatalf("locked state title = %q, want Advanced Reporting (Pro)", catalog.LockedState.Title) + if catalog.LockedState.Title != "Advanced Reporting" { + t.Fatalf("locked state title = %q, want Advanced Reporting", catalog.LockedState.Title) } if catalog.Guidance.Title != "Advanced Insights" { t.Fatalf("guidance title = %q, want Advanced Insights", catalog.Guidance.Title) diff --git a/scripts/release_control/subsystem_lookup_test.py b/scripts/release_control/subsystem_lookup_test.py index d76470dae..9b52d6564 100644 --- a/scripts/release_control/subsystem_lookup_test.py +++ b/scripts/release_control/subsystem_lookup_test.py @@ -3606,7 +3606,7 @@ class SubsystemLookupTest(unittest.TestCase): { "heading": "## Shared Boundaries", "path": "internal/api/access_control_handlers.go", - "line": 149, + "line": 156, "heading_line": 91, } ],