Skip to content

Commit

Permalink
upstream: b=main,r=ff6edd8951f81b4573f844984e3b37c8c543b09a,t=2024-03…
Browse files Browse the repository at this point in the history
…-22-1242-42785
  • Loading branch information
sonatype-zion committed Mar 22, 2024
1 parent 3d410f4 commit 5fa7bb2
Show file tree
Hide file tree
Showing 22 changed files with 343 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ protected void doApply(final Properties properties, final Path workDirPath) {
log.info("Loading Pro Starter Edition");
properties.setProperty(NEXUS_EDITION, getEdition().editionString);
properties.setProperty(NEXUS_FEATURES, getEditionFeature().featureString);
properties.setProperty("nexus.analytics.enabled", Boolean.TRUE.toString());
createEditionMarker(workDirPath, getEdition());
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Sonatype Nexus (TM) Open Source Version
* Copyright (c) 2008-present Sonatype, Inc.
* All rights reserved. Includes the third-party code listed at http://links.sonatype.com/products/nexus/oss/attributions.
*
* This program and the accompanying materials are made available under the terms of the Eclipse Public License Version 1.0,
* which accompanies this distribution and is available at http://www.eclipse.org/legal/epl-v10.html.
*
* Sonatype Nexus (TM) Professional Version is available from Sonatype, Inc. "Sonatype" and "Sonatype Nexus" are trademarks
* of Sonatype, Inc. Apache Maven is a trademark of the Apache Software Foundation. M2eclipse is a trademark of the
* Eclipse Foundation. All other trademarks are the property of their respective owners.
*/
package org.sonatype.nexus.common.analytics;

import java.util.Map;

public interface ContentUsage
{
Map<String, Long> getMetrics();

boolean shouldBlock();

void recalculate();

void recalculate(boolean blockingStateEventCounterEnable);

int getBlockingStateEventCounter();

void clearBlockingStateEventCounter();
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* of Sonatype, Inc. Apache Maven is a trademark of the Apache Software Foundation. M2eclipse is a trademark of the
* Eclipse Foundation. All other trademarks are the property of their respective owners.
*/
package org.sonatype.nexus.common.app;
package org.sonatype.nexus.common.analytics;

import java.util.Map;

Expand Down
1 change: 0 additions & 1 deletion plugins/nexus-blobstore-s3/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,6 @@
aws-java-sdk-sts,
aws-java-sdk-core,
aws-java-sdk-kms,
ion-java,
jmespath-java,
aws-java-sdk-cloudwatch,
aws-java-sdk-cloudwatchmetrics,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,20 +42,18 @@ export default function UsageMetrics() {
const isProEdition = ExtJS.isProEdition();
const isHa = ExtJS.state().getValue('nexus.datastore.clustered.enabled');
const isCircuitBreakerEnabled = ExtJS.state().getValue('nexus.circuitb.enabled');
const isAdmin = ExtJS.state().getUser()?.administrator;
const showMetricsWithMeter = isCircuitBreakerEnabled && isAdmin;

function retry() {
send('RETRY');
};

return !isHa && <div className={`nxrm-usage-metrics${showMetricsWithMeter ? '-circuit-breaker' : ''} nxrm-metrics-section`}>
return !isHa && <div className={`nxrm-usage-metrics${isCircuitBreakerEnabled ? '-circuit-breaker' : ''} nxrm-metrics-section`}>
<NxLoadWrapper loading={isLoading} error={loadError} retryHandler={retry}>
{() =>
<>
<NxH2>{MENU.TEXT}</NxH2>
<NxCard.Container>
{showMetricsWithMeter ? <UsageMetricsWithCircuitBreaker /> :
{isCircuitBreakerEnabled ? <UsageMetricsWithCircuitBreaker /> :
<>
<NxCard aria-label={TOTAL_COMPONENTS.TITLE}>
<NxCard.Header>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -361,15 +361,15 @@ describe('Usage Metrics', () => {

let infoIcon = selectors.getCardInfoIcon(totalComponentsCard);
await TestUtils.expectToSeeTooltipOnHover(infoIcon,
'Sonatype Nexus Repository OSS performs best when your total component counts remain under the threshold.');
'Sonatype Nexus Repository OSS performs best when your total component counts remain under 100,000 components across all repositories in your instance.');

infoIcon = selectors.getCardInfoIcon(uniqueLoginsCard);
await TestUtils.expectToSeeTooltipOnHover(infoIcon,
'Measures unique users who login over a period of time.');

infoIcon = selectors.getCardInfoIcon(reqsPerDayCard);
await TestUtils.expectToSeeTooltipOnHover(infoIcon,
'Sonatype Nexus Repository OSS performs best when requests per day remain under the threshold.');
'Sonatype Nexus Repository OSS performs best when requests per day remain under 20,000 requests per day to all repository endpoints across all repositories in your instance.');
});

it('renders tooltips when hovering on the info icon when PRO edition', async () => {
Expand All @@ -386,7 +386,7 @@ describe('Usage Metrics', () => {

infoIcon = selectors.getCardInfoIcon(reqsPerMinuteCard);
await TestUtils.expectToSeeTooltipOnHover(infoIcon,
'Measures requests per minute to your Sonatype Nexus Repository Pro instance.');
'Measures requests per minute to repository endpoints for all repositories in your Sonatype Nexus Repository Pro instance.');

infoIcon = selectors.getCardInfoIcon(reqsPerDayCard);
await TestUtils.expectToSeeTooltipOnHover(infoIcon,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
* Eclipse Foundation. All other trademarks are the property of their respective owners.
*/
import React from 'react';
import {NxButtonBar, NxErrorAlert, NxTextLink} from '@sonatype/react-shared-components';
import {NxButtonBar, NxErrorAlert, NxTextLink, NxWarningAlert} from '@sonatype/react-shared-components';
import {ExtJS} from '@sonatype/nexus-ui-plugin';
import {isEmpty} from 'ramda';

Expand All @@ -22,8 +22,12 @@ import './UsageMetricsAlert.scss';
const {
WELCOME: {
USAGE: {
CIRCUIT_BREAKER: {
PERCENTAGE
},
ALERTS: {
HARD_LIMITS,
WARNING_LIMITS,
LEARN_ABOUT_PRO,
REVIEW_YOUR_USAGE,
UPGRADING_PRO,
Expand All @@ -34,9 +38,9 @@ const COMPONENT_TOTAL_COUNT = 'component_total_count';
const REQUESTS_PER_DAY_HARD_LIMIT = 200_000;
const COMPONENT_COUNT_HARD_LIMIT = 120_000;

const MessageContent = function({metricMessage}){
const MessageContent = function({metricMessage, limit}){
return <p>
{metricMessage.PREFIX}
{metricMessage.PREFIX(limit)}
<NxTextLink external href={REVIEW_YOUR_USAGE.URL}>{REVIEW_YOUR_USAGE.TEXT}</NxTextLink>
{metricMessage.MID}
<NxTextLink external href={UPGRADING_PRO.URL}>{UPGRADING_PRO.TEXT}</NxTextLink>
Expand All @@ -46,16 +50,25 @@ const MessageContent = function({metricMessage}){

const AlertContent = function({metric, content}) {
return <>
{metric.metricName === PEAK_REQUESTS_PER_DAY && <MessageContent metricMessage={content.REQUESTS_PER_DAY}/>}
{metric.metricName === COMPONENT_TOTAL_COUNT && <MessageContent metricMessage={content.TOTAL_COMPONENTS}/>}
{metric.metricName === PEAK_REQUESTS_PER_DAY && <MessageContent metricMessage={content.REQUESTS_PER_DAY}
limit={REQUESTS_PER_DAY_HARD_LIMIT.toLocaleString()}/>}
{metric.metricName === COMPONENT_TOTAL_COUNT && <MessageContent metricMessage={content.TOTAL_COMPONENTS}
limit={COMPONENT_COUNT_HARD_LIMIT.toLocaleString()}/>}
</>
};

export default function UsageMetricsAlert() {
export default function UsageMetricsAlert({onClose}) {
const metrics = ExtJS.state().getValue('contentUsageEvaluationResult');
const hardLimitMetrics = metrics.filter(
m => (m.metricName === PEAK_REQUESTS_PER_DAY && m.metricValue >= REQUESTS_PER_DAY_HARD_LIMIT) ||
(m.metricName === COMPONENT_TOTAL_COUNT && m.metricValue >= COMPONENT_COUNT_HARD_LIMIT));
const hardLimitMetrics = metrics.filter(m =>
(m.metricName === PEAK_REQUESTS_PER_DAY && m.metricValue >= REQUESTS_PER_DAY_HARD_LIMIT) ||
(m.metricName === COMPONENT_TOTAL_COUNT && m.metricValue >= COMPONENT_COUNT_HARD_LIMIT)
);
const warningLimitMetrics = metrics.filter(m =>
(m.metricName === PEAK_REQUESTS_PER_DAY && m.metricValue >= REQUESTS_PER_DAY_HARD_LIMIT * PERCENTAGE) ||
(m.metricName === COMPONENT_TOTAL_COUNT && m.metricValue >= COMPONENT_COUNT_HARD_LIMIT * PERCENTAGE)
);
const showWarningAlert = isEmpty(hardLimitMetrics) && !isEmpty(warningLimitMetrics);

return <>
{!isEmpty(hardLimitMetrics) && <NxErrorAlert>
<div>
Expand All @@ -69,5 +82,10 @@ export default function UsageMetricsAlert() {
</a>
</NxButtonBar>
</NxErrorAlert>}
{showWarningAlert && <NxWarningAlert onClose={onClose} role="alert">
<div>
{warningLimitMetrics.map(m => <AlertContent key={m.metricName} metric={m} content={WARNING_LIMITS}/>)}
</div>
</NxWarningAlert>}
</>
};
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@
*/
import React from 'react';
import {render, screen} from '@testing-library/react';
import userEvent from "@testing-library/user-event";
import {when} from "jest-when";

import UsageMetricsAlert from './UsageMetricsAlert';
import TestUtils from '@sonatype/nexus-ui-plugin/src/frontend/src/interface/TestUtils';
import {ExtJS} from '@sonatype/nexus-ui-plugin';
import {HARD_LIMIT_REACHED} from './UsageMetricsAlert.testdata';
import {HARD_LIMIT_REACHED, WARNING_LIMIT_REACHED} from './UsageMetricsAlert.testdata';

jest.mock('@sonatype/nexus-ui-plugin', () => ({
...jest.requireActual('@sonatype/nexus-ui-plugin'),
Expand All @@ -31,7 +32,8 @@ jest.mock('@sonatype/nexus-ui-plugin', () => ({
const selectors = {
...TestUtils.selectors,
getAlert: () => screen.getByRole('alert'),
getLearnAboutLink: () => screen.getByRole('link', {name: 'Learn about Pro'})
getCloseButton: () => screen.getByRole('button', {name: 'Close'}),
getLinks: (t) => screen.getAllByRole('link', {name: t})
};

describe('Usage Metrics Alert', () => {
Expand All @@ -53,10 +55,57 @@ describe('Usage Metrics Alert', () => {
const requestsPerDayMessage = 'Users can not currently upload to this repository. This repository has hit ' +
'the maximum of 200,000 peak requests in the past 30 days. Review your usage and consider upgrading to Pro ' +
'for unlimited usage.';
const link = selectors.getLearnAboutLink();

expect(alert).toBeInTheDocument();
expect(alert).toHaveTextContent(componentCountMessage);
expect(alert).toHaveTextContent(requestsPerDayMessage);
expectLinksToBeRendered('Learn about Pro', 'Review your usage', 'upgrading to Pro');
});

it('renders warning limit alert when warning limit value reached', async () => {
await renderView(WARNING_LIMIT_REACHED);

const alert = selectors.getAlert();
const componentCountMessage = 'This repository is approaching the maximum of 120,000 components. ' +
'Users will not be able to upload to this repository after reaching this limit. ' +
'Review your usage and consider removing unused components or upgrading to Pro for unlimited usage.';
const requestsPerDayMessage = 'This repository is approaching the maximum of 200,000 peak requests in the past 30 days. ' +
'Users will not be able to upload to this repository after reaching this limit. ' +
'Review your usage and consider upgrading to Pro for unlimited usage.';

expect(alert).toBeInTheDocument();
expect(alert).toHaveTextContent(componentCountMessage);
expect(alert).toHaveTextContent(requestsPerDayMessage);
expect(link).toBeInTheDocument();
expectLinksToBeRendered('Review your usage', 'upgrading to Pro');
});

it("tests the close button in the alert", async () => {
const onClose = jest.fn();
await renderView(WARNING_LIMIT_REACHED, onClose);

const alert = selectors.getAlert();

expect(alert).toBeInTheDocument();
userEvent.click(selectors.getCloseButton());
expect(onClose).toBeCalled();
});
});

function expectLinksToBeRendered(...links) {

for (let l of links) {
const links = selectors.getLinks(l);
if (l === 'Learn about Pro') {
expect(links.length).toBe(1);
expect(links[0]).toHaveAttribute('href', 'https://links.sonatype.com/products/nxrm3/docs/learn-about-pro');
} else if (l === 'Review your usage') {
expect(links.length).toBe(2);
expect(links[0]).toHaveAttribute('href', 'https://links.sonatype.com/products/nxrm3/docs/review-usage');
expect(links[1]).toHaveAttribute('href', 'https://links.sonatype.com/products/nxrm3/docs/review-usage');
} else {
expect(links.length).toBe(2);
expect(links[0]).toHaveAttribute('href', 'https://links.sonatype.com/products/nxrm3/docs/upgrade-to-pro');
expect(links[1]).toHaveAttribute('href', 'https://links.sonatype.com/products/nxrm3/docs/upgrade-to-pro');
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,39 @@ export const HARD_LIMIT_REACHED = [
"limitLevel": "FREE_TIER"
}
];

export const WARNING_LIMIT_REACHED = [
{
"metricName": "peak_requests_per_day",
"metricValue": 150000,
"limits": [
{
"limitName": "SOFT_LIMIT",
"limitValue": 20000
}
],
"limitLevel": "SOFT_LIMIT"
},
{
"metricName": "component_total_count",
"metricValue": 90000,
"limits": [
{
"limitName": "SOFT_LIMIT",
"limitValue": 100000
}
],
"limitLevel": "SOFT_LIMIT"
},
{
"metricName": "successful_last_24h",
"metricValue": 26,
"limits": [
{
"limitName": "SOFT_LIMIT",
"limitValue": 75
}
],
"limitLevel": "FREE_TIER"
}
];
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ function Card({card, usage}) {
</NxCard>
};

function CardWithThreshold({card, usage, link, tooltip}) {
function CardWithThreshold({card, usage, link, tooltip, edition}) {
const {AGGREGATE_PERIOD_30_D, HIGHEST_RECORDED_COUNT, THRESHOLD, METRIC_NAME, SUB_TITLE, TITLE} = card;
const cardData = usage.find(m => m.metricName === METRIC_NAME);
const {aggregates, limits, metricValue} = cardData;
Expand All @@ -84,7 +84,7 @@ function CardWithThreshold({card, usage, link, tooltip}) {
<NxCard.Header>
<NxH3>
{TITLE}
<NxTooltip title={tooltip}>
<NxTooltip title={tooltip(softLimitValue.toLocaleString(), edition)}>
<NxFontAwesomeIcon icon={faInfoCircle}/>
</NxTooltip>
</NxH3>
Expand Down Expand Up @@ -120,7 +120,7 @@ function CardWithThreshold({card, usage, link, tooltip}) {
</NxCard>
};

function CardWithHardLimitThreshold({card, usage, link, tooltip}) {
function CardWithHardLimitThreshold({card, usage, link, tooltip, edition}) {
const {AGGREGATE_PERIOD_30_D, HIGHEST_RECORDED_COUNT, THRESHOLD, METRIC_NAME, SUB_TITLE, TITLE} = card;
const cardData = usage.find(m => m.metricName === METRIC_NAME);
const {aggregates, metricValue} = cardData;
Expand All @@ -140,7 +140,7 @@ function CardWithHardLimitThreshold({card, usage, link, tooltip}) {
<NxCard.Header>
<NxH3>
{TITLE}
<NxTooltip title={tooltip}>
<NxTooltip title={tooltip(card.HARD_LIMIT_VALUE.toLocaleString(), edition)}>
<NxFontAwesomeIcon icon={faInfoCircle}/>
</NxTooltip>
</NxH3>
Expand Down Expand Up @@ -224,21 +224,21 @@ export default function UsageMetricsWithCircuitBreaker() {
</>
} else if (isProEdition && !isPostgresql) {
return <>
<CardWithThreshold key={TOTAL_COMPONENTS.TITLE} card={TOTAL_COMPONENTS} usage={usage} link={CARD_LINK_PRO} tooltip={TOTAL_COMPONENTS.TOOLTIP_PRO}/>
<CardWithThreshold key={TOTAL_COMPONENTS.TITLE} card={TOTAL_COMPONENTS} usage={usage} link={CARD_LINK_PRO} tooltip={TOTAL_COMPONENTS.TOOLTIP} edition="PRO"/>
<CardWithoutThreshold key={REQUESTS_PER_MINUTE.TITLE} card={REQUESTS_PER_MINUTE} usage={usage} tooltip={REQUESTS_PER_MINUTE.TOOLTIP_PRO}/>
<CardWithThreshold key={REQUESTS_PER_DAY.TITLE} card={REQUESTS_PER_DAY} usage={usage} link={CARD_LINK_PRO} tooltip={REQUESTS_PER_DAY.TOOLTIP_PRO}/>
<CardWithThreshold key={REQUESTS_PER_DAY.TITLE} card={REQUESTS_PER_DAY} usage={usage} link={CARD_LINK_PRO} tooltip={REQUESTS_PER_DAY.TOOLTIP} edition="PRO"/>
</>
} else if (isProStarterEdition) {
return <>
<CardWithHardLimitThreshold key={TOTAL_COMPONENTS.TITLE} card={TOTAL_COMPONENTS} usage={usage} link={CARD_LINK_PRO_STARTER} tooltip={TOTAL_COMPONENTS.TOOLTIP_PRO_STARTER}/>
<CardWithHardLimitThreshold key={TOTAL_COMPONENTS.TITLE} card={TOTAL_COMPONENTS} usage={usage} link={CARD_LINK_PRO_STARTER} tooltip={TOTAL_COMPONENTS.TOOLTIP} edition="PRO-STARTER"/>
<CardWithoutThreshold key={UNIQUE_LOGINS.TITLE} card={UNIQUE_LOGINS} usage={usage} tooltip={UNIQUE_LOGINS.TOOLTIP_PRO_STARTER}/>
<CardWithHardLimitThreshold key={REQUESTS_PER_DAY.TITLE} card={REQUESTS_PER_DAY} usage={usage} link={CARD_LINK_PRO_STARTER} tooltip={REQUESTS_PER_DAY.TOOLTIP_PRO_STARTER}/>
<CardWithHardLimitThreshold key={REQUESTS_PER_DAY.TITLE} card={REQUESTS_PER_DAY} usage={usage} link={CARD_LINK_PRO_STARTER} tooltip={REQUESTS_PER_DAY.TOOLTIP} edition="PRO-STARTER"/>
</>
} else {
return <>
<CardWithThreshold key={TOTAL_COMPONENTS.TITLE} card={TOTAL_COMPONENTS} usage={usage} link={CARD_LINK_OSS} tooltip={TOTAL_COMPONENTS.TOOLTIP}/>
<CardWithThreshold key={TOTAL_COMPONENTS.TITLE} card={TOTAL_COMPONENTS} usage={usage} link={CARD_LINK_OSS} tooltip={TOTAL_COMPONENTS.TOOLTIP} edition="OSS"/>
<CardWithoutThreshold key={UNIQUE_LOGINS.TITLE} card={UNIQUE_LOGINS} usage={usage} tooltip={UNIQUE_LOGINS.TOOLTIP}/>
<CardWithThreshold key={REQUESTS_PER_DAY.TITLE} card={REQUESTS_PER_DAY} usage={usage} link={CARD_LINK_OSS} tooltip={REQUESTS_PER_DAY.TOOLTIP}/>
<CardWithThreshold key={REQUESTS_PER_DAY.TITLE} card={REQUESTS_PER_DAY} usage={usage} link={CARD_LINK_OSS} tooltip={REQUESTS_PER_DAY.TOOLTIP} edition="OSS"/>
</>
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
*/
import React, {useEffect, useRef, useState} from 'react';
import {useMachine} from '@xstate/react';
import {ExtJS, toURIParams, getVersionMajorMinor, Permissions} from '@sonatype/nexus-ui-plugin';
import {ExtJS, toURIParams, getVersionMajorMinor} from '@sonatype/nexus-ui-plugin';
import {
NxButton,
NxButtonBar,
Expand Down Expand Up @@ -64,8 +64,7 @@ export default function Welcome() {
usertype: getUserType(user),
daysToExpiry: ExtJS.useLicense().daysToExpiry
},
canViewMetrics = ExtJS.checkPermission(Permissions.METRICS.READ),
hasUser = getUserType(user) !== 'anonymous';
isAdmin = user?.administrator;

function load() {
send('LOAD');
Expand Down Expand Up @@ -136,7 +135,7 @@ export default function Welcome() {
</NxWarningAlert>
</section>
}
{canViewMetrics && hasUser && <UsageMetrics />}
{isAdmin && <UsageMetrics />}
<OutreachActions />
{ state.context.data?.showOutreachIframe &&
<iframe
Expand Down
Loading

0 comments on commit 5fa7bb2

Please sign in to comment.