1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-05-03 01:18:43 +02:00

feat/telemetry opt out (#4035)

<!-- Thanks for creating a PR! To make it easier for reviewers and
everyone else to understand what your changes relate to, please add some
relevant content to the headings below. Feel free to ignore or delete
sections that you don't think are relevant. Thank you! ❤️ -->

## About the changes
<!-- Describe the changes introduced. What are they and why are they
being introduced? Feel free to also add screenshots or steps to view the
changes if they're visual. -->

Adds a UI that shows current status of version and feature usage
collection configuration, and a presence in the configuration menu +
menu bar.

Configuring these features is done by setting environment variables. The
version info collection is an existing feature that we're making more
visible, the feature usage collection feature is a new feature that has
it's own environment configuration but also depends on version info
collection being active to work.

When version collection is turned off and the experimental feature flag
for feature usage collection is turned off:
<img width="1269" alt="image"
src="https://github.com/Unleash/unleash/assets/707867/435a07da-d238-4b5b-a150-07e3bd6b816f">


When version collection is turned on and the experimental feature flag
is off:
<img width="1249" alt="image"
src="https://github.com/Unleash/unleash/assets/707867/8d1a76c5-99c9-4551-9a4f-86d477bbbf6f">


When the experimental feature flag is enabled, and version+telemetry is
turned off:
<img width="1239" alt="image"
src="https://github.com/Unleash/unleash/assets/707867/e0bc532b-be94-4076-bee1-faef9bc48a5b">


When version collection is turned on, the experimental feature flag is
enabled, and telemetry collection is turned off:
<img width="1234" alt="image"
src="https://github.com/Unleash/unleash/assets/707867/1bd190c1-08fe-4402-bde3-56f863a33289">


When version collection is turned on, the experimental feature flag is
enabled, and telemetry collection is turned on:
<img width="1229" alt="image"
src="https://github.com/Unleash/unleash/assets/707867/848912cd-30bd-43cf-9b81-c58a4cbad1e4">


When version collection is turned off, the experimental feature flag is
enabled, and telemetry collection is turned on:
<img width="1241" alt="image"
src="https://github.com/Unleash/unleash/assets/707867/d2b981f2-033f-4fae-a115-f93e0653729b">

---------

Co-authored-by: sighphyre <liquidwicked64@gmail.com>
Co-authored-by: Nuno Góis <github@nunogois.com>
Co-authored-by: Thomas Heartman <thomas@getunleash.ai>
This commit is contained in:
David Leek 2023-06-30 08:43:58 +02:00 committed by GitHub
parent 73b4ae18c1
commit 3a14b97fdd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 567 additions and 0 deletions

View File

@ -10,6 +10,7 @@ import { EditGroupContainer } from './groups/EditGroup/EditGroup';
import { Group } from './groups/Group/Group';
import { GroupsAdmin } from './groups/GroupsAdmin';
import { InstanceAdmin } from './instance-admin/InstanceAdmin';
import { InstancePrivacy } from './instance-privacy/InstancePrivacy';
import { MaintenanceAdmin } from './maintenance';
import AdminMenu from './menu/AdminMenu';
import { Network } from './network/Network';
@ -46,6 +47,7 @@ export const Admin = () => (
<Route path="auth" element={<AuthSettings />} />
<Route path="admin-invoices" element={<FlaggedBillingRedirect />} />
<Route path="billing" element={<Billing />} />
<Route path="instance-privacy" element={<InstancePrivacy />} />
</Routes>
</>
);

View File

@ -0,0 +1,147 @@
import { PageContent } from 'component/common/PageContent/PageContent';
import { PageHeader } from 'component/common/PageHeader/PageHeader';
import { Box, styled } from '@mui/material';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { InstancePrivacySection } from './InstancePrivacySection';
import { useTelemetry } from 'hooks/api/getters/useTelemetry/useTelemetry';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
interface IFeatureActivenessManagementInfo {
enabled: IActivenessManagementInfo;
disabled: IActivenessManagementInfo;
}
interface IActivenessManagementInfo {
environmentVariables: string;
changeInfoText: string;
}
const StyledBox = styled(Box)(({ theme }) => ({
display: 'grid',
gap: theme.spacing(4),
}));
const versionCollectionDetails = {
title: 'Version data collection',
infoText:
"We collect the version of Unleash that you're using. We use this information to inform your Unleash instance of latest updates and critical security patches.",
concreteDetails: {
'Instance ID': 'A unique ID generated for your instance',
Version: "The version of Unleash that you're using",
},
};
const featureCollectionDetails = {
title: 'Feature data collection',
infoText:
'We collect data about your instance to improve the Unleash product user experience. We may also use the data in case you need help from our support team. Data collection is for internal use only and is not shared with third parties outside Unleash. As we want you to be in control of your data, we will leave it up to you to allow us to collect your data.',
concreteDetails: {
'Feature toggles': 'The number of feature toggles in your instance',
Users: 'The number of users registered in your instance',
Projects: 'The number of projects in your instance',
'Context Fields': 'The number of custom context fields in use',
Groups: 'The number of groups present in your instance',
Roles: 'The number of custom roles defined in your instance',
Environments: 'The number of environments in your instance',
Segments: 'The number of segments defined in your instance',
Strategies: 'The number of strategies defined in your instance',
'Feature Exports': 'The number of feature exports performed',
'Feature Imports': 'The number of feature imports performed',
'Custom Strategies':
'The number of custom strategies defined in your instance',
'Custom Strategies In Use':
'The number of custom strategies that are in use by feature toggles',
},
};
const versionCollectionActivenessManagementTexts: IFeatureActivenessManagementInfo =
{
enabled: {
environmentVariables: 'CHECK_VERSION=false',
changeInfoText:
'Version info collection can be disabled by setting the environment variable `CHECK_VERSION` to `false` and restarting Unleash.',
},
disabled: {
environmentVariables: 'CHECK_VERSION=true',
changeInfoText:
'Version info collection can be enabled by setting the environment variable to true and restarting Unleash.',
},
};
const featureCollectionActivenessManagementTexts: IFeatureActivenessManagementInfo =
{
enabled: {
environmentVariables: 'SEND_TELEMETRY=false',
changeInfoText:
'Feature usage collection can be disabled by setting the environment variable to false and restarting Unleash.',
},
disabled: {
environmentVariables: 'SEND_TELEMETRY=true',
changeInfoText:
'To enable feature usage collection set the environment variable to true and restart Unleash.',
},
};
export const InstancePrivacy = () => {
const { settings } = useTelemetry();
const { uiConfig, loading } = useUiConfig();
if (loading) {
return null;
}
const versionActivenessInfo = settings?.versionInfoCollectionEnabled
? versionCollectionActivenessManagementTexts.enabled
: versionCollectionActivenessManagementTexts.disabled;
const featureActivenessInfo = settings?.featureInfoCollectionEnabled
? featureCollectionActivenessManagementTexts.enabled
: featureCollectionActivenessManagementTexts.disabled;
let dependsOnFeatureCollection: undefined | string = undefined;
if (!settings?.versionInfoCollectionEnabled)
dependsOnFeatureCollection = settings?.featureInfoCollectionEnabled
? 'Note: Feature usage collection is enabled, but for it to be active you must also enable version info collection'
: 'When you enable feature usage collection you must also enable version info collection';
return (
<PageContent header={<PageHeader title="Instance Privacy" />}>
<StyledBox>
<InstancePrivacySection
title={versionCollectionDetails.title}
infoText={versionCollectionDetails.infoText}
concreteDetails={versionCollectionDetails.concreteDetails}
enabled={settings?.versionInfoCollectionEnabled}
changeInfoText={versionActivenessInfo.changeInfoText}
variablesText={versionActivenessInfo.environmentVariables}
/>
<ConditionallyRender
condition={Boolean(
uiConfig.flags.experimentalExtendedTelemetry
)}
show={
<InstancePrivacySection
title={featureCollectionDetails.title}
infoText={featureCollectionDetails.infoText}
concreteDetails={
featureCollectionDetails.concreteDetails
}
enabled={
settings?.featureInfoCollectionEnabled &&
settings?.versionInfoCollectionEnabled
}
changeInfoText={
featureActivenessInfo.changeInfoText
}
variablesText={
featureActivenessInfo.environmentVariables
}
dependsOnText={dependsOnFeatureCollection}
/>
}
/>
</StyledBox>
</PageContent>
);
};

View File

@ -0,0 +1,208 @@
import { Box, styled } from '@mui/material';
import { Badge } from 'component/common/Badge/Badge';
import ClearIcon from '@mui/icons-material/Clear';
import CheckIcon from '@mui/icons-material/Check';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { TooltipLink } from 'component/common/TooltipLink/TooltipLink';
const StyledContainer = styled('div')(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
padding: theme.spacing(3),
border: `1px solid ${theme.palette.divider}`,
borderRadius: theme.shape.borderRadiusLarge,
}));
const StyledCardTitleRow = styled(Box)(() => ({
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
}));
const StyledCardDescription = styled(Box)(({ theme }) => ({
color: theme.palette.text.secondary,
fontSize: theme.fontSizes.smallBody,
marginTop: theme.spacing(2),
}));
const StyledPropertyName = styled('p')(({ theme }) => ({
display: 'table-cell',
fontWeight: theme.fontWeight.bold,
paddingTop: theme.spacing(2),
}));
const StyledPropertyDetails = styled('p')(({ theme }) => ({
display: 'table-cell',
paddingTop: theme.spacing(2),
paddingLeft: theme.spacing(4),
}));
const StyledDataCollectionExplanation = styled('div')(({ theme }) => ({
display: 'table-cell',
width: '75%',
paddingTop: theme.spacing(2),
paddingBottom: theme.spacing(2),
}));
const StyledDataCollectionBadge = styled('div')(({ theme }) => ({
display: 'table-cell',
}));
const StyledTag = styled('span')(({ theme }) => ({
display: 'block',
textAlign: 'right',
color: theme.palette.neutral.dark,
}));
const StyledDescription = styled('div')(({ theme }) => ({
maxWidth: theme.spacing(50),
color: theme.palette.text.secondary,
fontSize: theme.fontSizes.smallBody,
}));
const StyledDataCollectionPropertyRow = styled('div')(() => ({
display: 'table-row',
}));
const StyledDataCollectionPropertyTable = styled('div')(() => ({
display: 'table',
}));
const StyledDataCollectionPropertyCell = styled('div')(() => ({
display: 'table-cell',
}));
interface IToolTipInstructionContentProps {
changeInfoText: string;
variablesText: string;
dependsOnText?: string;
}
const ToolTipInstructionContent = ({
changeInfoText,
variablesText,
dependsOnText,
}: IToolTipInstructionContentProps) => {
return (
<StyledDescription>
<ToolTipDescriptionText>{changeInfoText}</ToolTipDescriptionText>
<ToolTipDescriptionCode>
<div>{variablesText}</div>
</ToolTipDescriptionCode>
<ConditionallyRender
condition={Boolean(dependsOnText)}
show={
<ToolTipDescriptionText>
{dependsOnText}
</ToolTipDescriptionText>
}
/>
</StyledDescription>
);
};
const ToolTipDescriptionCode = styled('code')(({ theme }) => ({
display: 'block',
color: theme.palette.text.primary,
backgroundColor: theme.palette.background.application,
fontSize: theme.fontSizes.smallerBody,
marginTop: theme.spacing(1),
padding: theme.spacing(1),
border: `1px solid ${theme.palette.divider}`,
borderRadius: theme.shape.borderRadius,
borderWidth: 1,
wordWrap: 'break-word',
whiteSpace: 'pre-wrap',
fontFamily: 'monospace',
lineHeight: 1.5,
}));
const ToolTipDescriptionText = styled('p')(({ theme }) => ({
color: theme.palette.text.primary,
fontSize: theme.fontSizes.smallBody,
marginTop: theme.spacing(1),
}));
interface IInstancePrivacySectionProps {
title: string;
infoText: string;
concreteDetails: Record<string, string>;
enabled: boolean;
changeInfoText: string;
variablesText: string;
dependsOnText?: string;
}
export const InstancePrivacySection = ({
title,
infoText,
concreteDetails,
enabled,
changeInfoText,
variablesText,
dependsOnText,
}: IInstancePrivacySectionProps) => {
return (
<StyledContainer>
<StyledCardTitleRow>
<b>{title}</b>
<StyledDataCollectionBadge>
<ConditionallyRender
condition={enabled}
show={
<Badge color="success" icon={<CheckIcon />}>
Data is collected
</Badge>
}
elseShow={
<Badge color="neutral" icon={<ClearIcon />}>
No data is collected
</Badge>
}
/>
</StyledDataCollectionBadge>
</StyledCardTitleRow>
<StyledCardDescription>
<StyledDataCollectionPropertyTable>
<StyledDataCollectionExplanation>
{infoText}
</StyledDataCollectionExplanation>
<StyledDataCollectionPropertyCell>
<StyledTag>
<TooltipLink
tooltip={
<ToolTipInstructionContent
changeInfoText={changeInfoText}
variablesText={variablesText}
dependsOnText={dependsOnText}
/>
}
>
{enabled
? 'How to disable collecting data?'
: 'How to enable collecting data?'}
</TooltipLink>
</StyledTag>
</StyledDataCollectionPropertyCell>
</StyledDataCollectionPropertyTable>
<StyledDataCollectionPropertyTable>
{Object.entries(concreteDetails).map(([key, value]) => {
return (
<StyledDataCollectionPropertyRow key={key}>
<StyledPropertyName>{key}</StyledPropertyName>
<StyledPropertyDetails>
{value}
</StyledPropertyDetails>
</StyledDataCollectionPropertyRow>
);
})}
</StyledDataCollectionPropertyTable>
</StyledCardDescription>
</StyledContainer>
);
};

View File

@ -19,6 +19,7 @@ const StyledBox = styled(Box)(({ theme }) => ({
display: 'grid',
gap: theme.spacing(4),
}));
const MaintenancePage = () => {
const { loading } = useUiConfig();

View File

@ -119,6 +119,15 @@ function AdminMenu() {
}
/>
<Tab
value="instance-privacy"
label={
<CenteredNavLink to="/admin/instance-privacy">
Instance privacy
</CenteredNavLink>
}
/>
{isBilling && (
<Tab
value="billing"

View File

@ -505,6 +505,11 @@ export const adminMenuRoutes: INavigationMenuItem[] = [
title: 'Billing & invoices',
menu: { adminSettings: true, mode: ['pro'] },
},
{
path: '/admin/instance-privacy',
title: 'Instance privacy',
menu: { adminSettings: true },
},
];
export const getRoute = (path: string) =>

View File

@ -0,0 +1,39 @@
import useSWR from 'swr';
import { useMemo } from 'react';
import { formatApiPath } from 'utils/formatPath';
import handleErrorResponses from '../httpErrorResponseHandler';
export interface ITelemetrySettings {
versionInfoCollectionEnabled: boolean;
featureInfoCollectionEnabled: boolean;
}
export interface ITelemetrySettingsResponse {
settings: ITelemetrySettings;
refetchGroup: () => void;
loading: boolean;
error?: Error;
}
export const useTelemetry = (): ITelemetrySettingsResponse => {
const { data, error, mutate } = useSWR(
formatApiPath(`api/admin/telemetry/settings`),
fetcher
);
return useMemo(
() => ({
settings: data,
loading: !error && !data,
refetchGroup: () => mutate(),
error,
}),
[data, error, mutate]
);
};
const fetcher = (path: string) => {
return fetch(path)
.then(handleErrorResponses('Telemetry Settings'))
.then(res => res.json());
};

View File

@ -54,6 +54,7 @@ export interface IFlags {
advancedPlayground?: boolean;
customRootRoles?: boolean;
strategySplittedButton?: boolean;
experimentalExtendedTelemetry?: boolean;
}
export interface IVersionInfo {

View File

@ -141,6 +141,7 @@ import {
variantsSchema,
versionSchema,
advancedPlaygroundFeatureSchema,
telemetrySettingsSchema,
} from './spec';
import { IServerOption } from '../types';
import { mapValues, omitKeys } from '../util';
@ -339,6 +340,7 @@ export const schemas: UnleashSchemas = {
importTogglesValidateSchema,
importTogglesValidateItemSchema,
contextFieldStrategiesSchema,
telemetrySettingsSchema,
};
// Remove JSONSchema keys that would result in an invalid OpenAPI spec.

View File

@ -140,3 +140,4 @@ export * from './admin-count-schema';
export * from './advanced-playground-feature-schema';
export * from './advanced-playground-response-schema';
export * from './advanced-playground-request-schema';
export * from './telemetry-settings-schema';

View File

@ -0,0 +1,29 @@
import { FromSchema } from 'json-schema-to-ts';
export const telemetrySettingsSchema = {
$id: '#/components/schemas/telemetrySettingsSchema',
type: 'object',
additionalProperties: false,
required: ['versionInfoCollectionEnabled', 'featureInfoCollectionEnabled'],
description:
'Contains information about which settings are configured for version info collection and feature usage collection.',
properties: {
versionInfoCollectionEnabled: {
type: 'boolean',
description:
'Whether collection of version info is enabled/active.',
example: true,
},
featureInfoCollectionEnabled: {
type: 'boolean',
description:
'Whether collection of feature usage metrics is enabled/active.',
example: true,
},
},
components: {},
} as const;
export type TelemetrySettingsSchema = FromSchema<
typeof telemetrySettingsSchema
>;

View File

@ -124,6 +124,10 @@ const OPENAPI_TAGS = [
description:
'API for managing [change requests](https://docs.getunleash.io/reference/change-requests).',
},
{
name: 'Telemetry',
description: 'API for information about telemetry collection',
},
] as const;
// make the export mutable, so it can be used in a schema

View File

@ -26,6 +26,7 @@ import ConstraintsController from './constraints';
import PatController from './user/pat';
import { PublicSignupController } from './public-signup';
import InstanceAdminController from './instance-admin';
import TelemetryController from './telemetry';
import FavoritesController from './favorites';
import MaintenanceController from './maintenance';
import { createKnexTransactionStarter } from '../../db/transaction';
@ -137,6 +138,11 @@ class AdminApi extends Controller {
'/maintenance',
new MaintenanceController(config, services).router,
);
this.app.use(
'/telemetry',
new TelemetryController(config, services).router,
);
}
}

View File

@ -0,0 +1,66 @@
import { Response } from 'express';
import { OpenApiService } from 'lib/services';
import { IAuthRequest } from '../unleash-types';
import { IUnleashConfig } from '../../types/option';
import Controller from '../controller';
import { NONE } from '../../types/permissions';
import { IUnleashServices } from 'lib/types';
import { createResponseSchema } from '../../openapi/util/create-response-schema';
import {
telemetrySettingsSchema,
TelemetrySettingsSchema,
} from '../../openapi/spec/telemetry-settings-schema';
class TelemetryController extends Controller {
config: IUnleashConfig;
openApiService: OpenApiService;
constructor(
config: IUnleashConfig,
{ openApiService }: Pick<IUnleashServices, 'openApiService'>,
) {
super(config);
this.config = config;
this.openApiService = openApiService;
this.route({
method: 'get',
path: '/settings',
handler: this.getTelemetrySettings,
permission: NONE,
middleware: [
openApiService.validPath({
tags: ['Telemetry'],
summary: 'Get telemetry settings',
description:
'Provides the configured settings for [telemetry information collection](https://docs.getunleash.io/topics/data-collection)',
operationId: 'getTelemetrySettings',
responses: {
200: createResponseSchema('telemetrySettingsSchema'),
},
}),
],
});
}
async getTelemetrySettings(
req: IAuthRequest,
res: Response<TelemetrySettingsSchema>,
): Promise<void> {
this.openApiService.respondWithValidation(
200,
res,
telemetrySettingsSchema.$id,
{
versionInfoCollectionEnabled: this.config.versionCheck.enable,
featureInfoCollectionEnabled:
this.config.flagResolver.isEnabled(
'experimentalExtendedTelemetry',
) && this.config.telemetry,
},
);
}
}
export default TelemetryController;

View File

@ -5322,6 +5322,27 @@ Stats are divided into current and previous **windows**.
],
"type": "object",
},
"telemetrySettingsSchema": {
"additionalProperties": false,
"description": "Contains information about which settings are configured for version info collection and feature usage collection.",
"properties": {
"featureInfoCollectionEnabled": {
"description": "Whether collection of feature usage metrics is enabled/active.",
"example": true,
"type": "boolean",
},
"versionInfoCollectionEnabled": {
"description": "Whether collection of version info is enabled/active.",
"example": true,
"type": "boolean",
},
},
"required": [
"versionInfoCollectionEnabled",
"featureInfoCollectionEnabled",
],
"type": "object",
},
"toggleMaintenanceSchema": {
"properties": {
"enabled": {
@ -13088,6 +13109,28 @@ true,false,"[{""range"":""allTime"",""count"":15},{""range"":""30d"",""count"":9
],
},
},
"/api/admin/telemetry/settings": {
"get": {
"description": "Provides the configured settings for [telemetry information collection](https://docs.getunleash.io/topics/data-collection)",
"operationId": "getTelemetrySettings",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/telemetrySettingsSchema",
},
},
},
"description": "telemetrySettingsSchema",
},
},
"summary": "Get telemetry settings",
"tags": [
"Telemetry",
],
},
},
"/api/admin/ui-config": {
"get": {
"operationId": "getUiConfig",
@ -14587,6 +14630,10 @@ true,false,"[{""range"":""allTime"",""count"":15},{""range"":""30d"",""count"":9
"description": "Create, update, and delete [tags and tag types](https://docs.getunleash.io/reference/tags).",
"name": "Tags",
},
{
"description": "API for information about telemetry collection",
"name": "Telemetry",
},
{
"description": "Experimental endpoints that may change or disappear at any time.",
"name": "Unstable",