diff --git a/frontend/src/component/admin/Admin.tsx b/frontend/src/component/admin/Admin.tsx
index ffffd82d96..a0d556c1df 100644
--- a/frontend/src/component/admin/Admin.tsx
+++ b/frontend/src/component/admin/Admin.tsx
@@ -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 = () => (
} />
} />
} />
+ } />
>
);
diff --git a/frontend/src/component/admin/instance-privacy/InstancePrivacy.tsx b/frontend/src/component/admin/instance-privacy/InstancePrivacy.tsx
new file mode 100644
index 0000000000..e78b929ba4
--- /dev/null
+++ b/frontend/src/component/admin/instance-privacy/InstancePrivacy.tsx
@@ -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 (
+ }>
+
+
+
+ }
+ />
+
+
+ );
+};
diff --git a/frontend/src/component/admin/instance-privacy/InstancePrivacySection.tsx b/frontend/src/component/admin/instance-privacy/InstancePrivacySection.tsx
new file mode 100644
index 0000000000..3637d050b1
--- /dev/null
+++ b/frontend/src/component/admin/instance-privacy/InstancePrivacySection.tsx
@@ -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 (
+
+ {changeInfoText}
+
+
+ {variablesText}
+
+
+
+ {dependsOnText}
+
+ }
+ />
+
+ );
+};
+
+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;
+ enabled: boolean;
+ changeInfoText: string;
+ variablesText: string;
+ dependsOnText?: string;
+}
+
+export const InstancePrivacySection = ({
+ title,
+ infoText,
+ concreteDetails,
+ enabled,
+ changeInfoText,
+ variablesText,
+ dependsOnText,
+}: IInstancePrivacySectionProps) => {
+ return (
+
+
+ {title}
+
+ }>
+ Data is collected
+
+ }
+ elseShow={
+ }>
+ No data is collected
+
+ }
+ />
+
+
+
+
+
+
+ {infoText}
+
+
+
+
+ }
+ >
+ {enabled
+ ? 'How to disable collecting data?'
+ : 'How to enable collecting data?'}
+
+
+
+
+
+
+ {Object.entries(concreteDetails).map(([key, value]) => {
+ return (
+
+ {key}
+
+ {value}
+
+
+ );
+ })}
+
+
+
+ );
+};
diff --git a/frontend/src/component/admin/maintenance/index.tsx b/frontend/src/component/admin/maintenance/index.tsx
index 0d8ae73b72..f27f6e4291 100644
--- a/frontend/src/component/admin/maintenance/index.tsx
+++ b/frontend/src/component/admin/maintenance/index.tsx
@@ -19,6 +19,7 @@ const StyledBox = styled(Box)(({ theme }) => ({
display: 'grid',
gap: theme.spacing(4),
}));
+
const MaintenancePage = () => {
const { loading } = useUiConfig();
diff --git a/frontend/src/component/admin/menu/AdminMenu.tsx b/frontend/src/component/admin/menu/AdminMenu.tsx
index fe820fdab2..7b79869025 100644
--- a/frontend/src/component/admin/menu/AdminMenu.tsx
+++ b/frontend/src/component/admin/menu/AdminMenu.tsx
@@ -119,6 +119,15 @@ function AdminMenu() {
}
/>
+
+ Instance privacy
+
+ }
+ />
+
{isBilling && (
diff --git a/frontend/src/hooks/api/getters/useTelemetry/useTelemetry.ts b/frontend/src/hooks/api/getters/useTelemetry/useTelemetry.ts
new file mode 100644
index 0000000000..6e6643fdf1
--- /dev/null
+++ b/frontend/src/hooks/api/getters/useTelemetry/useTelemetry.ts
@@ -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());
+};
diff --git a/frontend/src/interfaces/uiConfig.ts b/frontend/src/interfaces/uiConfig.ts
index 1380393d28..a70419d12a 100644
--- a/frontend/src/interfaces/uiConfig.ts
+++ b/frontend/src/interfaces/uiConfig.ts
@@ -54,6 +54,7 @@ export interface IFlags {
advancedPlayground?: boolean;
customRootRoles?: boolean;
strategySplittedButton?: boolean;
+ experimentalExtendedTelemetry?: boolean;
}
export interface IVersionInfo {
diff --git a/src/lib/openapi/index.ts b/src/lib/openapi/index.ts
index 4e471b7f82..1e635fb6f7 100644
--- a/src/lib/openapi/index.ts
+++ b/src/lib/openapi/index.ts
@@ -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.
diff --git a/src/lib/openapi/spec/index.ts b/src/lib/openapi/spec/index.ts
index 479e0f887d..338ce3237d 100644
--- a/src/lib/openapi/spec/index.ts
+++ b/src/lib/openapi/spec/index.ts
@@ -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';
diff --git a/src/lib/openapi/spec/telemetry-settings-schema.ts b/src/lib/openapi/spec/telemetry-settings-schema.ts
new file mode 100644
index 0000000000..b27b03d318
--- /dev/null
+++ b/src/lib/openapi/spec/telemetry-settings-schema.ts
@@ -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
+>;
diff --git a/src/lib/openapi/util/openapi-tags.ts b/src/lib/openapi/util/openapi-tags.ts
index d48064d5ad..4a3e487f27 100644
--- a/src/lib/openapi/util/openapi-tags.ts
+++ b/src/lib/openapi/util/openapi-tags.ts
@@ -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
diff --git a/src/lib/routes/admin-api/index.ts b/src/lib/routes/admin-api/index.ts
index 1a2d38dc31..ef8f94937a 100644
--- a/src/lib/routes/admin-api/index.ts
+++ b/src/lib/routes/admin-api/index.ts
@@ -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,
+ );
}
}
diff --git a/src/lib/routes/admin-api/telemetry.ts b/src/lib/routes/admin-api/telemetry.ts
new file mode 100644
index 0000000000..d50004c574
--- /dev/null
+++ b/src/lib/routes/admin-api/telemetry.ts
@@ -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,
+ ) {
+ 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,
+ ): Promise {
+ this.openApiService.respondWithValidation(
+ 200,
+ res,
+ telemetrySettingsSchema.$id,
+ {
+ versionInfoCollectionEnabled: this.config.versionCheck.enable,
+ featureInfoCollectionEnabled:
+ this.config.flagResolver.isEnabled(
+ 'experimentalExtendedTelemetry',
+ ) && this.config.telemetry,
+ },
+ );
+ }
+}
+
+export default TelemetryController;
diff --git a/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap b/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap
index 5f30693f29..a1a2073394 100644
--- a/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap
+++ b/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap
@@ -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",