mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-11 14:00:29 +00:00
Remove customer commercial analytics wrappers
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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(() => <InvestigateAlertButton alert={makeAlert()} licenseLocked={true} />);
|
||||
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(() => <InvestigateAlertButton alert={makeAlert()} />);
|
||||
await fireEvent.click(screen.getByRole('button'));
|
||||
expect(trackUpgradeClickedMock).not.toHaveBeenCalled();
|
||||
expect(openUpgradeDestinationMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
@@ -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<AIRuntimeControlsSectionProps>
|
||||
<Show when={showAutonomousControlOption()}>
|
||||
<option value="autonomous">
|
||||
Autonomous - Pulse Assistant executes without approval
|
||||
{state.autoFixLocked() && state.showUpgradePrompts() ? ' (Pro)' : ''}
|
||||
</option>
|
||||
</Show>
|
||||
</FormSelect>
|
||||
@@ -289,7 +287,6 @@ export const AIRuntimeControlsSection: Component<AIRuntimeControlsSectionProps>
|
||||
<UpgradeLink
|
||||
class="text-blue-600 dark:text-blue-400 font-medium hover:underline"
|
||||
destination={state.upgradeAutofixDestination()}
|
||||
onClick={() => trackUpgradeClicked('settings_ai_patrol_autofix', 'ai_autofix')}
|
||||
>
|
||||
{UPGRADE_ACTION_LABEL}
|
||||
</UpgradeLink>{' '}
|
||||
|
||||
@@ -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 = () => {
|
||||
<Crown class="w-5 h-5 text-amber-600 dark:text-amber-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-base font-semibold text-base-content">Agent Profiles</h3>
|
||||
<p class="text-sm text-muted">Pro feature</p>
|
||||
<h3 class="text-base font-semibold text-base-content">
|
||||
{featureGateCopy.title}
|
||||
</h3>
|
||||
<p class="text-sm text-muted">{featureGateCopy.subtitle}</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-sm text-muted">
|
||||
Create reusable configuration profiles for your agents. Manage settings like Docker
|
||||
monitoring, logging levels, and reporting intervals from a central location.
|
||||
</p>
|
||||
<p class="text-sm text-muted">{featureGateCopy.body}</p>
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<UpgradeLink
|
||||
destination={getUpgradeActionDestination('agent_profiles')}
|
||||
class={getUpgradeActionButtonClass({ tone: 'warning', mobileFullWidth: false })}
|
||||
onClick={() =>
|
||||
trackUpgradeClicked('settings_agent_profiles_panel', 'agent_profiles')
|
||||
}
|
||||
>
|
||||
<Crown class="w-4 h-4" />
|
||||
{UPGRADE_ACTION_LABEL}
|
||||
|
||||
@@ -50,7 +50,6 @@ export default function AuditLogPanel() {
|
||||
goToLastPage,
|
||||
goToNextPage,
|
||||
goToPreviousPage,
|
||||
handleUpgradeClick,
|
||||
hasNextPage,
|
||||
hasResumeEvents,
|
||||
hasSignedEvents,
|
||||
@@ -170,7 +169,6 @@ export default function AuditLogPanel() {
|
||||
<UpgradeLink
|
||||
destination={upgradeDestination()}
|
||||
class={getUpgradeActionButtonClass({ mobileFullWidth: false })}
|
||||
onClick={handleUpgradeClick}
|
||||
>
|
||||
{UPGRADE_ACTION_LABEL}
|
||||
</UpgradeLink>
|
||||
|
||||
@@ -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<AuditWebhookPanelProps> = (props) => {
|
||||
<UpgradeLink
|
||||
destination={upgradeDestination()}
|
||||
class={getUpgradeActionButtonClass()}
|
||||
onClick={() =>
|
||||
trackUpgradeClicked('settings_audit_webhook_panel', 'audit_logging')
|
||||
}
|
||||
>
|
||||
{UPGRADE_ACTION_LABEL}
|
||||
</UpgradeLink>
|
||||
|
||||
@@ -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<ConnectionEditorProps> = (props) => {
|
||||
const [selectedCandidate, setSelectedCandidate] = createSignal<ProbeCandidate | null>(
|
||||
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<ConnectionEditorProps> = (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<ConnectionEditorProps> = (props) => {
|
||||
onSelectCandidate={chooseCandidate}
|
||||
onInstallAgent={installAgent}
|
||||
onChooseSourceTypeInstead={props.onBackToCatalog}
|
||||
onProbeSubmitted={() => onboardingMetrics?.recordPathSelected('api')}
|
||||
onProbeResolved={(outcome) => onboardingMetrics?.recordProbeResult(outcome)}
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
+5
-40
@@ -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) => <div data-testid="slot">slot:{props.type}</div>);
|
||||
|
||||
render(() => <ConnectionEditor renderCredentialSlot={renderSlot} onClose={() => {}} />);
|
||||
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) => <div data-testid="slot">slot:{props.type}</div>);
|
||||
|
||||
render(() => (
|
||||
<ConnectionEditor
|
||||
initialType="truenas"
|
||||
trackInitialCatalogSelection={true}
|
||||
onboardingMetricsTracker={externalTracker}
|
||||
renderCredentialSlot={renderSlot}
|
||||
onClose={() => {}}
|
||||
/>
|
||||
));
|
||||
|
||||
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');
|
||||
|
||||
@@ -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 = () => {
|
||||
<For each={section.snippets}>
|
||||
{(snippet) => {
|
||||
const copyCommand = () => snippet.command;
|
||||
const commandTelemetryCapability = () => {
|
||||
const label = normalizeTelemetryPart(snippet.label) || 'install';
|
||||
return `${section.platform}:${state.installProfile()}:${label}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="space-y-2">
|
||||
@@ -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(),
|
||||
);
|
||||
|
||||
@@ -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<InfrastructureWorkspaceProps> =
|
||||
if (readOnly()) return null;
|
||||
return deriveAddStepFromLocation(location.pathname, location.search ?? '');
|
||||
});
|
||||
const [addFlowTracker, setAddFlowTracker] =
|
||||
createSignal<InfrastructureOnboardingMetricsTracker | null>(
|
||||
routeStep() ? createOpenedOnboardingTracker() : null,
|
||||
);
|
||||
let previousRouteStep: InfrastructurePanelStep | null | undefined = routeStep();
|
||||
const activeAddType = createMemo<ConnectionType | null>(() => {
|
||||
const step = routeStep();
|
||||
if (!step || step === 'pick' || step === 'detect') return null;
|
||||
@@ -198,27 +182,18 @@ const InfrastructureWorkspaceContent: Component<InfrastructureWorkspaceProps> =
|
||||
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<InfrastructureWorkspaceProps> =
|
||||
resetInlineEditorState();
|
||||
setSelectedDiscoveredSource(null);
|
||||
setSelectedProbeCandidate(null);
|
||||
setAddFlowTracker(null);
|
||||
clearSharedInfrastructureOnboardingMetricsTracker();
|
||||
navigateToWorkspace(Boolean(routeStep()));
|
||||
};
|
||||
|
||||
@@ -255,33 +228,12 @@ const InfrastructureWorkspaceContent: Component<InfrastructureWorkspaceProps> =
|
||||
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<InfrastructureWorkspaceProps> =
|
||||
<Match when={routeStep() === 'pick'}>
|
||||
<InfrastructureSourcePicker
|
||||
onSelectType={(type) => {
|
||||
recordCatalogSelection(type);
|
||||
openAddFlow(type === 'agent' ? 'agent' : (type as ManagedAddTypeStep));
|
||||
}}
|
||||
onDetectFromAddress={() => openAddFlow('detect')}
|
||||
@@ -960,7 +911,6 @@ const InfrastructureWorkspaceContent: Component<InfrastructureWorkspaceProps> =
|
||||
<Match when={routeStep() === 'detect'}>
|
||||
<ConnectionEditor
|
||||
mode="add"
|
||||
onboardingMetricsTracker={addFlowTracker()}
|
||||
onBackToCatalog={() => openAddFlow('pick')}
|
||||
onSelectAgentRoute={() => openAddFlow('agent')}
|
||||
onSelectCandidate={openAddFlowFromProbe}
|
||||
@@ -978,12 +928,6 @@ const InfrastructureWorkspaceContent: Component<InfrastructureWorkspaceProps> =
|
||||
initialType={activeAddType() ?? undefined}
|
||||
initialCandidate={selectedProbeCandidate()}
|
||||
showSlotHeader={false}
|
||||
trackInitialCatalogSelection={
|
||||
activeAddType() !== 'agent' &&
|
||||
!selectedDiscoveredSource() &&
|
||||
!selectedProbeCandidate()
|
||||
}
|
||||
onboardingMetricsTracker={addFlowTracker()}
|
||||
onBackToCatalog={() => openAddFlow('pick')}
|
||||
onClose={closeAddFlow}
|
||||
onSaved={handleAddSaved}
|
||||
|
||||
@@ -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()}
|
||||
/>
|
||||
|
||||
|
||||
@@ -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<ProLicensePlanSectionProps> = (pro
|
||||
<UpgradeLink
|
||||
class="inline-flex items-center gap-1 mt-3 min-h-10 sm:min-h-9 rounded-md border border-current/20 px-3 py-2 text-xs font-medium hover:bg-black/5 dark:hover:bg-white/5"
|
||||
destination={action().destination}
|
||||
onClick={props.onPurchaseActivationActionClick}
|
||||
>
|
||||
{action().label}
|
||||
</UpgradeLink>
|
||||
@@ -171,7 +168,6 @@ export const ProLicensePlanSection: Component<ProLicensePlanSectionProps> = (pro
|
||||
<UpgradeLink
|
||||
class="inline-flex items-center gap-1 mt-3 min-h-10 sm:min-h-9 rounded-md border border-current/20 px-3 py-2 text-xs font-medium hover:bg-black/5 dark:hover:bg-white/5"
|
||||
destination={prompt().actionDestination}
|
||||
onClick={props.onPlanSelectionPromptClick}
|
||||
>
|
||||
{prompt().actionLabel}
|
||||
</UpgradeLink>
|
||||
@@ -295,7 +291,6 @@ export const ProLicensePlanSection: Component<ProLicensePlanSectionProps> = (pro
|
||||
<UpgradeLink
|
||||
class="inline-flex items-center gap-1 mt-4 min-h-10 sm:min-h-9 rounded-md border border-border px-3 py-2 text-xs font-medium text-base-content hover:bg-surface-hover"
|
||||
destination={action().destination}
|
||||
onClick={props.onPlanSelectionPromptClick}
|
||||
>
|
||||
{action().label}
|
||||
</UpgradeLink>
|
||||
|
||||
@@ -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<RBACFeatureGateSectionProps> = (p
|
||||
<UpgradeLink
|
||||
destination={getUpgradeActionDestination('rbac')}
|
||||
class={getUpgradeActionButtonClass()}
|
||||
onClick={() => trackUpgradeClicked(props.paywallLocation, 'rbac')}
|
||||
>
|
||||
{UPGRADE_ACTION_LABEL}
|
||||
</UpgradeLink>
|
||||
|
||||
@@ -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<RelaySettingsPanelProps> = (props) =>
|
||||
<UpgradeLink
|
||||
destination={state.upgradeDestination()}
|
||||
class={RELAY_PRIMARY_LINK_CLASS}
|
||||
onClick={() => trackUpgradeClicked('settings_relay_panel', 'relay')}
|
||||
>
|
||||
View plans
|
||||
</UpgradeLink>
|
||||
|
||||
@@ -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() {
|
||||
<UpgradeLink
|
||||
destination={upgradeDestination()}
|
||||
class={getUpgradeActionButtonClass()}
|
||||
onClick={() =>
|
||||
trackUpgradeClicked('settings_reporting_panel', reportingCatalog()!.id)
|
||||
}
|
||||
>
|
||||
{UPGRADE_ACTION_LABEL}
|
||||
</UpgradeLink>
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
getSSOProviderSummary,
|
||||
getSSOProviderTypeBadgeClass,
|
||||
getSSOProviderTypeLabel,
|
||||
getSSOSamlFeatureGateCopy,
|
||||
} from '@/utils/ssoProviderPresentation';
|
||||
import {
|
||||
getUpgradeActionButtonClass,
|
||||
@@ -40,6 +41,7 @@ interface SSOProvidersPanelProps {
|
||||
}
|
||||
|
||||
export const SSOProvidersPanel: Component<SSOProvidersPanelProps> = (props) => {
|
||||
const samlFeatureGateCopy = getSSOSamlFeatureGateCopy();
|
||||
const {
|
||||
providers,
|
||||
loading,
|
||||
@@ -78,7 +80,6 @@ export const SSOProvidersPanel: Component<SSOProvidersPanelProps> = (props) => {
|
||||
getUpgradeActionDestination,
|
||||
runtimeCapabilitiesLoaded,
|
||||
showUpgradePrompts,
|
||||
trackUpgradeClicked,
|
||||
} = useSSOProvidersState(props);
|
||||
|
||||
return (
|
||||
@@ -95,7 +96,7 @@ export const SSOProvidersPanel: Component<SSOProvidersPanelProps> = (props) => {
|
||||
<div class="flex items-center justify-between px-6 py-4 border-b border-border">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-base-content">Add SAML Provider</h3>
|
||||
<p class="text-xs text-muted mt-0.5">Pro feature</p>
|
||||
<p class="text-xs text-muted mt-0.5">{samlFeatureGateCopy.subtitle}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@@ -107,7 +108,7 @@ export const SSOProvidersPanel: Component<SSOProvidersPanelProps> = (props) => {
|
||||
</button>
|
||||
</div>
|
||||
<div class="px-6 py-5 space-y-4">
|
||||
<p class="text-sm text-muted">SAML 2.0 and multi-provider SSO requires Pro.</p>
|
||||
<p class="text-sm text-muted">{samlFeatureGateCopy.body}</p>
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
@@ -119,9 +120,6 @@ export const SSOProvidersPanel: Component<SSOProvidersPanelProps> = (props) => {
|
||||
<UpgradeLink
|
||||
destination={getUpgradeActionDestination('advanced_sso')}
|
||||
class={getUpgradeActionButtonClass({ mobileFullWidth: false })}
|
||||
onClick={() =>
|
||||
trackUpgradeClicked('settings_sso_providers_add_saml_gate', 'advanced_sso')
|
||||
}
|
||||
>
|
||||
{UPGRADE_ACTION_LABEL}
|
||||
<ExternalLink class="w-4 h-4" />
|
||||
@@ -141,17 +139,15 @@ export const SSOProvidersPanel: Component<SSOProvidersPanelProps> = (props) => {
|
||||
<div class="p-4 bg-surface-alt border border-border rounded-md">
|
||||
<div class="flex flex-col sm:flex-row items-center gap-4">
|
||||
<div class="flex-1">
|
||||
<h4 class="text-base font-semibold text-base-content">Advanced SSO</h4>
|
||||
<p class="text-sm text-muted mt-1">
|
||||
SAML 2.0 and multi-provider SSO requires Pro. Basic OIDC is available in the free
|
||||
tier.
|
||||
</p>
|
||||
<h4 class="text-base font-semibold text-base-content">
|
||||
{samlFeatureGateCopy.title}
|
||||
</h4>
|
||||
<p class="text-sm text-muted mt-1">{samlFeatureGateCopy.body}</p>
|
||||
</div>
|
||||
<div class="flex flex-col sm:flex-row items-center gap-2">
|
||||
<UpgradeLink
|
||||
destination={getUpgradeActionDestination('advanced_sso')}
|
||||
class={getUpgradeActionButtonClass({ mobileFullWidth: false })}
|
||||
onClick={() => trackUpgradeClicked('settings_sso_providers_banner', 'advanced_sso')}
|
||||
>
|
||||
{UPGRADE_ACTION_LABEL}
|
||||
<ExternalLink class="w-4 h-4" />
|
||||
@@ -195,7 +191,7 @@ export const SSOProvidersPanel: Component<SSOProvidersPanelProps> = (props) => {
|
||||
class="min-h-10 sm:min-h-9 px-3 py-2.5 text-sm font-medium border border-border text-base-content rounded-md hover:bg-surface-hover transition-colors flex items-center gap-1.5"
|
||||
>
|
||||
<Plus class="w-4 h-4" />
|
||||
{getSSOProviderAddButtonLabel('saml', true)}
|
||||
{getSSOProviderAddButtonLabel('saml')}
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -90,11 +90,6 @@ vi.mock('@/utils/logger', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@/utils/upgradeMetrics', () => ({
|
||||
trackPaywallViewed: vi.fn(),
|
||||
trackUpgradeClicked: vi.fn(),
|
||||
}));
|
||||
|
||||
const makeAgentResource = (overrides: Partial<Resource> = {}): Resource => ({
|
||||
id: 'hash-agent-resource-id',
|
||||
type: 'agent',
|
||||
|
||||
+20
@@ -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'
|
||||
|
||||
@@ -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<typeof vi.fn>;
|
||||
recordPathSelected: ReturnType<typeof vi.fn>;
|
||||
recordProbeResult: ReturnType<typeof vi.fn>;
|
||||
recordCatalogSelected: ReturnType<typeof vi.fn>;
|
||||
recordCredentialsOpened: ReturnType<typeof vi.fn>;
|
||||
}>,
|
||||
);
|
||||
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: () => <div data-testid="agent-profiles">profiles</div>,
|
||||
}));
|
||||
|
||||
vi.mock('@/utils/infrastructureOnboardingMetrics', () => ({
|
||||
createInfrastructureOnboardingMetricsTracker: createInfrastructureOnboardingMetricsTrackerMock,
|
||||
getSharedInfrastructureOnboardingMetricsTracker: createInfrastructureOnboardingMetricsTrackerMock,
|
||||
clearSharedInfrastructureOnboardingMetricsTracker: vi.fn(),
|
||||
}));
|
||||
|
||||
const connectionFixture = (overrides: Partial<Connection> = {}): 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 () => {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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(() => <RolesPanel />);
|
||||
|
||||
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(() => <UserAssignmentsPanel />);
|
||||
|
||||
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(() => <RolesPanel />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Custom Roles (Pro)')).toBeInTheDocument();
|
||||
expect(screen.getByText('Custom Roles')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.getByRole('link', { name: 'View plans' })).toBeInTheDocument();
|
||||
|
||||
@@ -18,16 +18,12 @@ vi.mock('../ResourcePicker', () => ({
|
||||
}): JSX.Element => <div>Mock Resource Picker</div>,
|
||||
}));
|
||||
|
||||
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: {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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('<ConnectionsTable rows={rows} />');
|
||||
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');
|
||||
|
||||
@@ -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: () => <div data-testid="quick-security-setup">Quick Security Setup</div>,
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -6,7 +6,6 @@ import { type PlatformConnectionsView } from './platformConnectionsModel';
|
||||
import { buildInfrastructureWorkspacePath } from './infrastructureWorkspaceModel';
|
||||
|
||||
export const TOKEN_PLACEHOLDER = '<api-token>';
|
||||
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) => {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { SettingsTab } from './settingsNavigationModel';
|
||||
import { trackPaywallViewed } from '@/utils/upgradeMetrics';
|
||||
import { getFeatureMinTierLabel } from '@/utils/licensePresentation';
|
||||
|
||||
export const tabFeatureRequirements: Partial<Record<SettingsTab, string[]>> = {
|
||||
@@ -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}.`;
|
||||
}
|
||||
|
||||
@@ -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<AISettingsType | null>(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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<typeof setInterval> | undefined;
|
||||
|
||||
const resetPairingState = () => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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<CompleteStepProps> = (props) => {
|
||||
const [copied, setCopied] = createSignal<'password' | 'admin-token' | null>(null);
|
||||
@@ -44,7 +42,6 @@ export const SetupCompletionPanel: Component<CompleteStepProps> = (props) => {
|
||||
const [connectedSystems, setConnectedSystems] = createSignal<
|
||||
ReturnType<typeof buildSetupCompletionConnectedSystems>
|
||||
>([]);
|
||||
let firstAgentConnectionTracked = false;
|
||||
|
||||
createEffect(() => {
|
||||
if (props.connectedResourcesOverride !== undefined) {
|
||||
@@ -81,14 +78,6 @@ export const SetupCompletionPanel: Component<CompleteStepProps> = (props) => {
|
||||
);
|
||||
});
|
||||
|
||||
if (
|
||||
!firstAgentConnectionTracked &&
|
||||
nextConnectedSystems.some((system) => system.connectionPath === 'agent')
|
||||
) {
|
||||
trackAgentFirstConnected(SETUP_WIZARD_TELEMETRY_SURFACE, 'first_agent');
|
||||
firstAgentConnectionTracked = true;
|
||||
}
|
||||
|
||||
if (hasConnectionChanges) {
|
||||
setConnectedSystems(nextConnectedSystems);
|
||||
}
|
||||
|
||||
+2
@@ -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');
|
||||
});
|
||||
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -28,7 +28,6 @@ export const MonitoredSystemLimitWarningBanner: Component = () => {
|
||||
<UpgradeLink
|
||||
class="text-xs font-medium underline underline-offset-2 hover:opacity-90"
|
||||
destination={state.installCollectorsDestination()}
|
||||
onClick={state.handleInstallCollectorsClick}
|
||||
>
|
||||
{MONITORED_SYSTEM_LIMIT_INSTALL_COLLECTORS_LABEL}
|
||||
</UpgradeLink>
|
||||
|
||||
@@ -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',
|
||||
|
||||
+2
-16
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 (
|
||||
<div class="space-y-8">
|
||||
<PageHeader
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Navigate, useLocation } from '@solidjs/router';
|
||||
import { Show, createMemo, onMount } from 'solid-js';
|
||||
import { PageHeader } from '@/components/shared/PageHeader';
|
||||
import { trackPaywallViewed } from '@/utils/upgradeMetrics';
|
||||
import {
|
||||
getPricingRouteDestination,
|
||||
handoffToExternalPricing,
|
||||
@@ -25,7 +24,6 @@ export default function PricingHandoff() {
|
||||
);
|
||||
|
||||
onMount(() => {
|
||||
trackPaywallViewed('pricing', 'pricing_handoff');
|
||||
if (externalDestination() || selfHostedPurchaseStartDestination()) {
|
||||
handoffToExternalPricing(destination());
|
||||
}
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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,
|
||||
}));
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<typeof import('@/utils/pricingHandoff')>('@/utils/pricingHandoff');
|
||||
@@ -23,7 +18,6 @@ vi.mock('@/utils/pricingHandoff', async () => {
|
||||
|
||||
describe('PricingHandoff', () => {
|
||||
beforeEach(() => {
|
||||
trackPaywallViewedMock.mockReset();
|
||||
handoffToExternalPricingMock.mockReset();
|
||||
window.scrollTo = vi.fn();
|
||||
});
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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'),
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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.';
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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.',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
@@ -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.
|
||||
}
|
||||
@@ -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.',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user