Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[LaunchDarkly] Add Deployment Metadata #143002

Merged
merged 6 commits into from
Oct 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -170,12 +170,16 @@ export default function ({ getService }: PluginFunctionalProviderContext) {
'xpack.cloud.base_url (string)',
'xpack.cloud.cname (string)',
'xpack.cloud.deployment_url (string)',
'xpack.cloud.is_elastic_staff_owned (boolean)',
'xpack.cloud.trial_end_date (string)',
'xpack.cloud_integrations.chat.chatURL (string)',
// No PII. This is an escape patch to override LaunchDarkly's flag resolution mechanism for testing or quick fix.
'xpack.cloud_integrations.experiments.flag_overrides (record)',
// Commented because it's inside a schema conditional, and the test is not able to resolve it. But it's shared.
// Added here for documentation purposes.
// 'xpack.cloud_integrations.experiments.launch_darkly.client_id (string)',
// 'xpack.cloud_integrations.experiments.launch_darkly.client_log_level (string)',
'xpack.cloud_integrations.experiments.metadata_refresh_interval (duration)',
'xpack.cloud_integrations.full_story.org_id (any)',
// No PII. Just the list of event types we want to forward to FullStory.
'xpack.cloud_integrations.full_story.eventTypesAllowlist (array)',
Expand Down
12 changes: 11 additions & 1 deletion x-pack/plugins/cloud/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,14 @@ This is the path to the Cloud Account and Billing page. The value is already pre

This value is the same as `baseUrl` on ESS but can be customized on ECE.

**Example:** `cloud.elastic.co` (on ESS)
**Example:** `cloud.elastic.co` (on ESS)

### `trial_end_date`

The end date for the Elastic Cloud trial. Only available on Elastic Cloud.

**Example:** `2020-10-14T10:40:22Z`

### `is_elastic_staff_owned`

`true` if the deployment is owned by an Elastician. Only available on Elastic Cloud.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import { firstValueFrom } from 'rxjs';
import { registerCloudDeploymentIdAnalyticsContext } from './register_cloud_deployment_id_analytics_context';
import { registerCloudDeploymentMetadataAnalyticsContext } from './register_cloud_deployment_id_analytics_context';

describe('registerCloudDeploymentIdAnalyticsContext', () => {
let analytics: { registerContextProvider: jest.Mock };
Expand All @@ -17,14 +17,16 @@ describe('registerCloudDeploymentIdAnalyticsContext', () => {
});

test('it does not register the context provider if cloudId not provided', () => {
registerCloudDeploymentIdAnalyticsContext(analytics);
registerCloudDeploymentMetadataAnalyticsContext(analytics, {});
expect(analytics.registerContextProvider).not.toHaveBeenCalled();
});

test('it registers the context provider and emits the cloudId', async () => {
registerCloudDeploymentIdAnalyticsContext(analytics, 'cloud_id');
registerCloudDeploymentMetadataAnalyticsContext(analytics, { id: 'cloud_id' });
expect(analytics.registerContextProvider).toHaveBeenCalledTimes(1);
const [{ context$ }] = analytics.registerContextProvider.mock.calls[0];
await expect(firstValueFrom(context$)).resolves.toEqual({ cloudId: 'cloud_id' });
await expect(firstValueFrom(context$)).resolves.toEqual({
cloudId: 'cloud_id',
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,44 @@
import type { AnalyticsClient } from '@kbn/analytics-client';
import { of } from 'rxjs';

export function registerCloudDeploymentIdAnalyticsContext(
export interface CloudDeploymentMetadata {
id?: string;
trial_end_date?: string;
is_elastic_staff_owned?: boolean;
}

export function registerCloudDeploymentMetadataAnalyticsContext(
analytics: Pick<AnalyticsClient, 'registerContextProvider'>,
cloudId?: string
cloudMetadata: CloudDeploymentMetadata
) {
if (!cloudId) {
if (!cloudMetadata.id) {
return;
}
const {
id: cloudId,
trial_end_date: cloudTrialEndDate,
is_elastic_staff_owned: cloudIsElasticStaffOwned,
} = cloudMetadata;

analytics.registerContextProvider({
name: 'Cloud Deployment ID',
context$: of({ cloudId }),
name: 'Cloud Deployment Metadata',
context$: of({ cloudId, cloudTrialEndDate, cloudIsElasticStaffOwned }),
schema: {
cloudId: {
type: 'keyword',
_meta: { description: 'The Cloud Deployment ID' },
},
cloudTrialEndDate: {
type: 'date',
_meta: { description: 'When the Elastic Cloud trial ends/ended', optional: true },
},
cloudIsElasticStaffOwned: {
type: 'boolean',
_meta: {
description: '`true` if the owner of the deployment is an Elastician',
optional: true,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why optional? is it only for BWC reasons on the schema?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This information comes from the cloud configuration. It may not be provided by the cloud deployment configuration (mind the schema.maybe).

},
},
},
});
}
2 changes: 2 additions & 0 deletions x-pack/plugins/cloud/public/mocks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ function createSetupMock() {
deploymentUrl: 'deployment-url',
profileUrl: 'profile-url',
organizationUrl: 'organization-url',
isElasticStaffOwned: true,
trialEndDate: new Date('2020-10-01T14:13:12Z'),
registerCloudService: jest.fn(),
};
}
Expand Down
57 changes: 49 additions & 8 deletions x-pack/plugins/cloud/public/plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import React, { FC } from 'react';
import type { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/public';

import { registerCloudDeploymentIdAnalyticsContext } from '../common/register_cloud_deployment_id_analytics_context';
import { registerCloudDeploymentMetadataAnalyticsContext } from '../common/register_cloud_deployment_id_analytics_context';
import { getIsCloudEnabled } from '../common/is_cloud_enabled';
import { ELASTIC_SUPPORT_LINK, CLOUD_SNAPSHOTS_PATH } from '../common/constants';
import { getFullCloudUrl } from './utils';
Expand All @@ -20,11 +20,8 @@ export interface CloudConfigType {
profile_url?: string;
deployment_url?: string;
organization_url?: string;
full_story: {
enabled: boolean;
org_id?: string;
eventTypesAllowlist?: string[];
};
trial_end_date?: string;
is_elastic_staff_owned?: boolean;
}

export interface CloudStart {
Expand Down Expand Up @@ -55,14 +52,50 @@ export interface CloudStart {
}

export interface CloudSetup {
/**
* Cloud ID. Undefined if not running on Cloud.
*/
cloudId?: string;
/**
* This value is the same as `baseUrl` on ESS but can be customized on ECE.
*/
cname?: string;
/**
* This is the URL of the Cloud interface.
*/
baseUrl?: string;
/**
* The full URL to the deployment management page on Elastic Cloud. Undefined if not running on Cloud.
*/
deploymentUrl?: string;
/**
* The full URL to the user profile page on Elastic Cloud. Undefined if not running on Cloud.
*/
profileUrl?: string;
/**
* The full URL to the organization management page on Elastic Cloud. Undefined if not running on Cloud.
*/
organizationUrl?: string;
/**
* This is the path to the Snapshots page for the deployment to which the Kibana instance belongs. The value is already prepended with `deploymentUrl`.
*/
snapshotsUrl?: string;
/**
* `true` when Kibana is running on Elastic Cloud.
*/
isCloudEnabled: boolean;
/**
* When the Cloud Trial ends/ended for the organization that owns this deployment. Only available when running on Elastic Cloud.
*/
trialEndDate?: Date;
/**
* `true` if the Elastic Cloud organization that owns this deployment is owned by an Elastician. Only available when running on Elastic Cloud.
*/
isElasticStaffOwned?: boolean;
/**
* Registers CloudServiceProviders so start's `CloudContextProvider` hooks them.
* @param contextProvider The React component from the Service Provider.
*/
registerCloudService: (contextProvider: FC) => void;
}

Expand All @@ -84,15 +117,23 @@ export class CloudPlugin implements Plugin<CloudSetup> {
}

public setup(core: CoreSetup): CloudSetup {
registerCloudDeploymentIdAnalyticsContext(core.analytics, this.config.id);
registerCloudDeploymentMetadataAnalyticsContext(core.analytics, this.config);

const { id, cname, base_url: baseUrl } = this.config;
const {
id,
cname,
base_url: baseUrl,
trial_end_date: trialEndDate,
is_elastic_staff_owned: isElasticStaffOwned,
} = this.config;

return {
cloudId: id,
cname,
baseUrl,
...this.getCloudUrls(),
trialEndDate: trialEndDate ? new Date(trialEndDate) : undefined,
isElasticStaffOwned,
isCloudEnabled: this.isCloudEnabled,
registerCloudService: (contextProvider) => {
this.contextProviders.push(contextProvider);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,51 @@
* 2.0.
*/

import {
createCollectorFetchContextMock,
usageCollectionPluginMock,
} from '@kbn/usage-collection-plugin/server/mocks';
import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server';
import { createCloudUsageCollector } from './cloud_usage_collector';
import { createCollectorFetchContextMock } from '@kbn/usage-collection-plugin/server/mocks';
import { CollectorFetchContext } from '@kbn/usage-collection-plugin/server';

const mockUsageCollection = () => ({
makeUsageCollector: jest.fn().mockImplementation((args: any) => ({ ...args })),
});
describe('createCloudUsageCollector', () => {
let usageCollection: UsageCollectionSetup;
let collectorFetchContext: jest.Mocked<CollectorFetchContext>;

const getMockConfigs = (isCloudEnabled: boolean) => ({ isCloudEnabled });
beforeEach(() => {
usageCollection = usageCollectionPluginMock.createSetupContract();
collectorFetchContext = createCollectorFetchContextMock();
});

describe('createCloudUsageCollector', () => {
it('calls `makeUsageCollector`', () => {
const mockConfigs = getMockConfigs(false);
const usageCollection = mockUsageCollection();
createCloudUsageCollector(usageCollection as any, mockConfigs);
createCloudUsageCollector(usageCollection, { isCloudEnabled: false });
expect(usageCollection.makeUsageCollector).toBeCalledTimes(1);
});

describe('Fetched Usage data', () => {
it('return isCloudEnabled boolean', async () => {
const mockConfigs = getMockConfigs(true);
const usageCollection = mockUsageCollection() as any;
const collector = createCloudUsageCollector(usageCollection, mockConfigs);
const collectorFetchContext = createCollectorFetchContextMock();
const collector = createCloudUsageCollector(usageCollection, { isCloudEnabled: true });

expect(await collector.fetch(collectorFetchContext)).toStrictEqual({
isCloudEnabled: true,
isElasticStaffOwned: undefined,
trialEndDate: undefined,
});
});

it('return inTrial boolean if trialEndDateIsProvided', async () => {
const collector = createCloudUsageCollector(usageCollection, {
isCloudEnabled: true,
trialEndDate: '2020-10-01T14:30:16Z',
});

expect((await collector.fetch(collectorFetchContext)).isCloudEnabled).toBe(true); // Adding the await because the fetch can be a Promise or a synchronous method and TS complains in the test if not awaited
expect(await collector.fetch(collectorFetchContext)).toStrictEqual({
isCloudEnabled: true,
isElasticStaffOwned: undefined,
trialEndDate: '2020-10-01T14:30:16Z',
inTrial: false,
});
});
});
});
14 changes: 13 additions & 1 deletion x-pack/plugins/cloud/server/collectors/cloud_usage_collector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,35 @@ import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server';

interface Config {
isCloudEnabled: boolean;
trialEndDate?: string;
isElasticStaffOwned?: boolean;
}

interface CloudUsage {
isCloudEnabled: boolean;
trialEndDate?: string;
inTrial?: boolean;
isElasticStaffOwned?: boolean;
}

export function createCloudUsageCollector(usageCollection: UsageCollectionSetup, config: Config) {
const { isCloudEnabled } = config;
const { isCloudEnabled, trialEndDate, isElasticStaffOwned } = config;
const trialEndDateMs = trialEndDate ? new Date(trialEndDate).getTime() : undefined;
return usageCollection.makeUsageCollector<CloudUsage>({
type: 'cloud',
isReady: () => true,
schema: {
isCloudEnabled: { type: 'boolean' },
trialEndDate: { type: 'date' },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit confused here, it seems that CloudUsage defines the trialEndDate as a string, whereas this schema defines it as a Date. But then looking at the fetch it would seem that we return a string. 🤔

Copy link
Member Author

@afharo afharo Oct 14, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

haha! yeah... type: 'date' is a string that's supposed to have the format of a date. It's more a declarative intention for the schema reader in the Telemetry receiving side.

inTrial: { type: 'boolean' },
isElasticStaffOwned: { type: 'boolean' },
},
fetch: () => {
return {
isCloudEnabled,
isElasticStaffOwned,
trialEndDate,
...(trialEndDateMs ? { inTrial: Date.now() <= trialEndDateMs } : {}),
};
},
});
Expand Down
4 changes: 4 additions & 0 deletions x-pack/plugins/cloud/server/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ const configSchema = schema.object({
id: schema.maybe(schema.string()),
organization_url: schema.maybe(schema.string()),
profile_url: schema.maybe(schema.string()),
trial_end_date: schema.maybe(schema.string()),
is_elastic_staff_owned: schema.maybe(schema.boolean()),
});

export type CloudConfigType = TypeOf<typeof configSchema>;
Expand All @@ -38,6 +40,8 @@ export const config: PluginConfigDescriptor<CloudConfigType> = {
id: true,
organization_url: true,
profile_url: true,
trial_end_date: true,
is_elastic_staff_owned: true,
},
schema: configSchema,
};
2 changes: 2 additions & 0 deletions x-pack/plugins/cloud/server/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ function createSetupMock(): jest.Mocked<CloudSetup> {
instanceSizeMb: 1234,
deploymentId: 'deployment-id',
isCloudEnabled: true,
isElasticStaffOwned: true,
trialEndDate: new Date('2020-10-01T14:13:12Z'),
apm: {
url: undefined,
secretToken: undefined,
Expand Down
Loading