implement analytics too

Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
Fredrik Adelöw
2024-10-08 17:51:14 +02:00
parent 40bfc240c7
commit 811ff0cddc
23 changed files with 196 additions and 153 deletions
@@ -24,7 +24,7 @@ import {
} from '@backstage/plugin-catalog-react';
import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils';
import {
MockAnalyticsApi,
mockApis,
renderInTestApp,
TestApiProvider,
TestApiRegistry,
@@ -212,9 +212,9 @@ describe('<CatalogGraphCard/>', () => {
],
}));
const analyticsSpy = new MockAnalyticsApi();
const analyticsApi = mockApis.analytics();
await renderInTestApp(
<TestApiProvider apis={[[analyticsApiRef, analyticsSpy]]}>
<TestApiProvider apis={[[analyticsApiRef, analyticsApi]]}>
{wrapper}
</TestApiProvider>,
{
@@ -228,12 +228,14 @@ describe('<CatalogGraphCard/>', () => {
expect(await screen.findByText('b:d/c')).toBeInTheDocument();
await userEvent.click(await screen.findByText('b:d/c'));
expect(analyticsSpy.getEvents()[0]).toMatchObject({
action: 'click',
subject: 'b:d/c',
attributes: {
to: '/entity/{kind}/{namespace}/{name}',
},
});
expect(analyticsApi.captureEvent).toHaveBeenCalledWith(
expect.objectContaining({
action: 'click',
subject: 'b:d/c',
attributes: {
to: '/entity/{kind}/{namespace}/{name}',
},
}),
);
});
});
@@ -23,7 +23,7 @@ import { analyticsApiRef } from '@backstage/core-plugin-api';
import { catalogApiRef, entityRouteRef } from '@backstage/plugin-catalog-react';
import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils';
import {
MockAnalyticsApi,
mockApis,
renderInTestApp,
TestApiProvider,
} from '@backstage/test-utils';
@@ -227,9 +227,9 @@ describe.skip('<CatalogGraphPage/>', () => {
}),
);
const analyticsSpy = new MockAnalyticsApi();
const analyticsApi = mockApis.analytics();
await renderInTestApp(
<TestApiProvider apis={[[analyticsApiRef, analyticsSpy]]}>
<TestApiProvider apis={[[analyticsApiRef, analyticsApi]]}>
{wrapper}
</TestApiProvider>,
{
@@ -243,10 +243,12 @@ describe.skip('<CatalogGraphPage/>', () => {
await userEvent.click(screen.getByText('b:d/e'));
expect(analyticsSpy.getEvents()[0]).toMatchObject({
action: 'click',
subject: 'b:d/e',
});
expect(analyticsApi.captureEvent).toHaveBeenCalledWith(
expect.objectContaining({
action: 'click',
subject: 'b:d/e',
}),
);
});
test('should capture analytics event when navigating to entity', async () => {
@@ -256,9 +258,9 @@ describe.skip('<CatalogGraphPage/>', () => {
}),
);
const analyticsSpy = new MockAnalyticsApi();
const analyticsApi = mockApis.analytics();
await renderInTestApp(
<TestApiProvider apis={[[analyticsApiRef, analyticsSpy]]}>
<TestApiProvider apis={[[analyticsApiRef, analyticsApi]]}>
{wrapper}
</TestApiProvider>,
{
@@ -274,12 +276,14 @@ describe.skip('<CatalogGraphPage/>', () => {
await user.keyboard('{Shift>}');
await user.click(screen.getByText('b:d/e'));
expect(analyticsSpy.getEvents()[0]).toMatchObject({
action: 'click',
subject: 'b:d/e',
attributes: {
to: '/entity/b/d/e',
},
});
expect(analyticsApi.captureEvent).toHaveBeenCalledWith(
expect.objectContaining({
action: 'click',
subject: 'b:d/e',
attributes: {
to: '/entity/b/d/e',
},
}),
);
});
});
@@ -25,7 +25,7 @@ import {
import { Entity } from '@backstage/catalog-model';
import { analyticsApiRef, useAnalytics } from '@backstage/core-plugin-api';
import {
MockAnalyticsApi,
mockApis,
TestApiRegistry,
withLogCollector,
} from '@backstage/test-utils';
@@ -57,7 +57,7 @@ describe('useEntity', () => {
});
it('should provide entityRef analytics context', () => {
const analyticsSpy = new MockAnalyticsApi();
const analyticsSpy = mockApis.analytics();
const apis = TestApiRegistry.from([analyticsApiRef, analyticsSpy]);
const { result } = renderHook(() => useAnalytics(), {
wrapper: ({ children }: React.PropsWithChildren<{}>) => (
@@ -69,9 +69,13 @@ describe('useEntity', () => {
result.current.captureEvent('test', 'value');
expect(analyticsSpy.getEvents()[0]).toMatchObject({
context: { entityRef: 'mykind:default/my-entity' },
});
expect(analyticsSpy.captureEvent).toHaveBeenCalledWith(
expect.objectContaining({
context: expect.objectContaining({
entityRef: 'mykind:default/my-entity',
}),
}),
);
});
});
@@ -127,7 +131,7 @@ describe('useAsyncEntity', () => {
});
it('should provide entityRef analytics context', () => {
const analyticsSpy = new MockAnalyticsApi();
const analyticsSpy = mockApis.analytics();
const apis = TestApiRegistry.from([analyticsApiRef, analyticsSpy]);
const { result } = renderHook(() => useAnalytics(), {
wrapper: ({ children }: React.PropsWithChildren<{}>) => (
@@ -144,13 +148,13 @@ describe('useAsyncEntity', () => {
result.current.captureEvent('test', 'value');
expect(analyticsSpy.getEvents()[0]).toMatchObject({
context: { entityRef: 'mykind:default/my-entity' },
});
expect(analyticsSpy.captureEvent.mock.calls[0][0].context.entityRef).toBe(
'mykind:default/my-entity',
);
});
it('should omit entityRef analytics context', () => {
const analyticsSpy = new MockAnalyticsApi();
const analyticsSpy = mockApis.analytics();
const apis = TestApiRegistry.from([analyticsApiRef, analyticsSpy]);
const { result } = renderHook(() => useAnalytics(), {
wrapper: ({ children }: PropsWithChildren<{}>) => (
@@ -162,6 +166,8 @@ describe('useAsyncEntity', () => {
result.current.captureEvent('test', 'value');
expect(analyticsSpy.getEvents()[0].context).not.toHaveProperty('entityRef');
expect(
analyticsSpy.captureEvent.mock.calls[0][0].context,
).not.toHaveProperty('entityRef');
});
});
@@ -16,7 +16,7 @@
import { ApiProvider } from '@backstage/core-app-api';
import {
MockAnalyticsApi,
mockApis,
renderInTestApp,
TestApiRegistry,
} from '@backstage/test-utils';
@@ -42,7 +42,7 @@ const scaffolderApiMock: jest.Mocked<ScaffolderApi> = {
const catalogApi = catalogApiMock.mock();
const analyticsMock = new MockAnalyticsApi();
const analyticsMock = mockApis.analytics();
const apis = TestApiRegistry.from(
[scaffolderApiRef, scaffolderApiMock],
[catalogApiRef, catalogApi],
@@ -17,7 +17,7 @@
import { ApiProvider } from '@backstage/core-app-api';
import { analyticsApiRef } from '@backstage/core-plugin-api';
import {
MockAnalyticsApi,
mockApis,
renderInTestApp,
TestApiRegistry,
} from '@backstage/test-utils';
@@ -56,12 +56,12 @@ const scaffolderApiMock: jest.Mocked<ScaffolderApi> = {
};
const catalogApi = catalogApiMock.mock();
const analyticsApi = mockApis.analytics();
const analyticsMock = new MockAnalyticsApi();
const apis = TestApiRegistry.from(
[scaffolderApiRef, scaffolderApiMock],
[catalogApiRef, catalogApi],
[analyticsApiRef, analyticsMock],
[analyticsApiRef, analyticsApi],
[catalogApiRef, catalogApi],
);
@@ -81,6 +81,7 @@ const entityRefResponse = {
},
},
};
describe('TemplateWizardPage', () => {
it('captures expected analytics events', async () => {
scaffolderApiMock.scaffold.mockResolvedValue({ taskId: 'xyz' });
@@ -130,20 +131,29 @@ describe('TemplateWizardPage', () => {
});
// The "Next Step" button should have fired an event
expect(analyticsMock.getEvents()[0]).toMatchObject({
action: 'click',
subject: 'Next Step (1)',
context: { entityRef: 'template:default/test' },
});
expect(analyticsApi.captureEvent).toHaveBeenCalledWith(
expect.objectContaining({
action: 'click',
subject: 'Next Step (1)',
context: expect.objectContaining({
entityRef: 'template:default/test',
}),
}),
);
// And the "Create" button should have fired an event
expect(analyticsMock.getEvents()[1]).toMatchObject({
action: 'create',
subject: 'expected-name',
context: { entityRef: 'template:default/test' },
value: 120,
});
expect(analyticsApi.captureEvent).toHaveBeenCalledWith(
expect.objectContaining({
action: 'create',
subject: 'expected-name',
context: expect.objectContaining({
entityRef: 'template:default/test',
}),
value: 120,
}),
);
});
describe('scaffolder page context menu', () => {
it('should render if editUrl is set to url', async () => {
catalogApi.getEntityByRef.mockResolvedValue({
@@ -175,6 +185,7 @@ describe('TemplateWizardPage', () => {
);
expect(queryByTestId('menu-button')).toBeInTheDocument();
});
it('should not render if editUrl is undefined', async () => {
catalogApi.getEntityByRef.mockResolvedValue({
apiVersion: 'v1',
@@ -20,7 +20,7 @@ import userEvent from '@testing-library/user-event';
import { configApiRef } from '@backstage/core-plugin-api';
import { ConfigReader } from '@backstage/core-app-api';
import {
MockAnalyticsApi,
mockApis,
TestApiProvider,
renderInTestApp,
} from '@backstage/test-utils';
@@ -273,7 +273,7 @@ describe('SearchBar', () => {
});
it('Does not capture analytics event if not enabled in app', async () => {
const analyticsApiMock = new MockAnalyticsApi();
const analyticsApiMock = mockApis.analytics();
await renderInTestApp(
<TestApiProvider
@@ -296,7 +296,7 @@ describe('SearchBar', () => {
await waitFor(() => expect(textbox).toHaveValue(value));
expect(analyticsApiMock.getEvents()).toHaveLength(0);
expect(analyticsApiMock.captureEvent).not.toHaveBeenCalled();
});
it('Renders custom search icon', async () => {
@@ -24,7 +24,7 @@ import DocsIcon from '@material-ui/icons/InsertDriveFile';
import {
renderInTestApp,
TestApiProvider,
MockAnalyticsApi,
mockApis,
} from '@backstage/test-utils';
import { createPlugin, analyticsApiRef } from '@backstage/core-plugin-api';
@@ -40,7 +40,7 @@ import {
const query = jest.fn().mockResolvedValue({ results: [] });
const searchApiMock = { query };
const analyticsApiMock = new MockAnalyticsApi();
const analyticsApiMock = mockApis.analytics();
describe('SearchResultGroup', () => {
const results = [
@@ -20,7 +20,7 @@ import { screen, waitFor } from '@testing-library/react';
import {
TestApiProvider,
renderInTestApp,
MockAnalyticsApi,
mockApis,
} from '@backstage/test-utils';
import { analyticsApiRef, createPlugin } from '@backstage/core-plugin-api';
@@ -31,7 +31,7 @@ import { SearchResultList } from './SearchResultList';
const query = jest.fn().mockResolvedValue({ results: [] });
const searchApiMock = { query };
const analyticsApiMock = new MockAnalyticsApi();
const analyticsApiMock = mockApis.analytics();
describe('SearchResultList', () => {
const results = [
@@ -422,9 +422,7 @@ describe('SearchContext', () => {
describe('analytics', () => {
it('captures analytics events if enabled in app', async () => {
const analyticsApiMock = {
captureEvent: jest.fn(),
} satisfies typeof analyticsApiRef.T;
const analyticsApiMock = mockApis.analytics();
searchApiMock.query.mockResolvedValue({
results: [],
@@ -481,9 +479,7 @@ describe('SearchContext', () => {
});
it('captures analytics events even if number of results does not exist', async () => {
const analyticsApiMock = {
captureEvent: jest.fn(),
} satisfies typeof analyticsApiRef.T;
const analyticsApiMock = mockApis.analytics();
searchApiMock.query.mockResolvedValue({
results: [],
+4 -4
View File
@@ -23,7 +23,7 @@ import ListItemText from '@material-ui/core/ListItemText';
import {
renderInTestApp,
TestApiProvider,
MockAnalyticsApi,
mockApis,
} from '@backstage/test-utils';
import {
createPlugin,
@@ -38,7 +38,7 @@ import {
SearchResultListItemExtensionOptions,
} from './extensions';
const analyticsApiMock = new MockAnalyticsApi();
const analyticsApiMock = mockApis.analytics();
const results = [
{
@@ -118,7 +118,7 @@ describe('extensions', () => {
screen.getByRole('link', { name: /Search Result 1/ }),
);
expect(analyticsApiMock.getEvents()[0]).toMatchObject({
expect(analyticsApiMock.captureEvent).toHaveBeenCalledWith({
action: 'discover',
subject: 'Search Result 1',
context: { routeRef: 'unknown', pluginId: 'root', extension: 'App' },
@@ -141,7 +141,7 @@ describe('extensions', () => {
await userEvent.click(screen.getByRole('listitem'));
expect(analyticsApiMock.getEvents()[0]).toMatchObject({
expect(analyticsApiMock.captureEvent).toHaveBeenCalledWith({
action: 'discover',
subject: 'Search Result 1',
context: { routeRef: 'unknown', pluginId: 'root', extension: 'App' },
@@ -19,7 +19,7 @@ import { screen, fireEvent, waitFor } from '@testing-library/react';
import { analyticsApiRef } from '@backstage/core-plugin-api';
import {
MockAnalyticsApi,
mockApis,
TestApiProvider,
renderInTestApp,
} from '@backstage/test-utils';
@@ -55,11 +55,11 @@ const defaultGitlabProps = {
};
describe('FeedbackLink', () => {
const apiSpy = new MockAnalyticsApi();
const analytics = mockApis.analytics();
it('Should open new Github issue tab', async () => {
await renderInTestApp(
<TestApiProvider apis={[[analyticsApiRef, apiSpy]]}>
<TestApiProvider apis={[[analyticsApiRef, analytics]]}>
<IssueLink {...defaultGithubProps} />
</TestApiProvider>,
);
@@ -77,7 +77,7 @@ describe('FeedbackLink', () => {
it('Should open new Gitlab issue tab', async () => {
await renderInTestApp(
<TestApiProvider apis={[[analyticsApiRef, apiSpy]]}>
<TestApiProvider apis={[[analyticsApiRef, analytics]]}>
<IssueLink {...defaultGitlabProps} />
</TestApiProvider>,
);
@@ -95,7 +95,7 @@ describe('FeedbackLink', () => {
it('Should track click events', async () => {
await renderInTestApp(
<TestApiProvider apis={[[analyticsApiRef, apiSpy]]}>
<TestApiProvider apis={[[analyticsApiRef, analytics]]}>
<IssueLink {...defaultGithubProps} />
</TestApiProvider>,
);
@@ -103,10 +103,12 @@ describe('FeedbackLink', () => {
fireEvent.click(screen.getByText(/Open new Github issue/));
await waitFor(() => {
expect(apiSpy.getEvents()[0]).toMatchObject({
action: 'click',
subject: 'Open new Github issue',
});
expect(analytics.captureEvent).toHaveBeenCalledWith(
expect.objectContaining({
action: 'click',
subject: 'Open new Github issue',
}),
);
});
});
});
+5 -9
View File
@@ -20,11 +20,7 @@ import { renderHook, act, waitFor } from '@testing-library/react';
import { ThemeProvider } from '@material-ui/core/styles';
import { lightTheme } from '@backstage/theme';
import {
MockAnalyticsApi,
mockApis,
TestApiProvider,
} from '@backstage/test-utils';
import { mockApis, TestApiProvider } from '@backstage/test-utils';
import { Entity, CompoundEntityRef } from '@backstage/catalog-model';
import {
analyticsApiRef,
@@ -66,7 +62,7 @@ const techdocsApiMock = {
getTechDocsMetadata: jest.fn().mockResolvedValue(mockTechDocsMetadata),
};
const analyticsApiMock = new MockAnalyticsApi();
const analyticsApiMock = mockApis.analytics();
const wrapper = ({
entityRef = {
@@ -170,12 +166,12 @@ describe('useTechDocsReaderPage', () => {
wrapper,
});
await waitFor(() => {
expect(analyticsApiMock.getEvents()[0]).toMatchObject({
expect(analyticsApiMock.captureEvent).toHaveBeenCalledWith({
action: 'action',
subject: 'subject',
context: {
context: expect.objectContaining({
entityRef: 'component:default/test',
},
}),
});
});
});
@@ -16,12 +16,11 @@
import { TechDocsNotFound } from './TechDocsNotFound';
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import { screen, waitFor } from '@testing-library/react';
import {
MockAnalyticsApi,
mockApis,
TestApiProvider,
renderInTestApp,
wrapInTestApp,
} from '@backstage/test-utils';
import { analyticsApiRef } from '@backstage/core-plugin-api';
@@ -58,18 +57,16 @@ describe('<TechDocsNotFound />', () => {
});
it('should trigger analytics event not-found', async () => {
const mockAnalyticsApi = new MockAnalyticsApi();
const mockAnalyticsApi = mockApis.analytics();
render(
wrapInTestApp(
<TestApiProvider apis={[[analyticsApiRef, mockAnalyticsApi]]}>
<TechDocsNotFound />
</TestApiProvider>,
),
await renderInTestApp(
<TestApiProvider apis={[[analyticsApiRef, mockAnalyticsApi]]}>
<TechDocsNotFound />
</TestApiProvider>,
);
await waitFor(() => {
expect(mockAnalyticsApi.getEvents()[0]).toMatchObject({
expect(mockAnalyticsApi.captureEvent).toHaveBeenCalledWith({
action: 'not-found',
subject: '/the/pathname?the=search#the-anchor',
attributes: {
@@ -77,6 +74,7 @@ describe('<TechDocsNotFound />', () => {
namespace: 'namespace',
kind: 'kind',
},
context: expect.anything(),
});
});
});