Introduce an entityRef context key

Signed-off-by: Eric Peterson <ericpeterson@spotify.com>
This commit is contained in:
Eric Peterson
2022-10-13 14:27:50 +02:00
parent be1b686c40
commit a889314692
4 changed files with 81 additions and 9 deletions
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-react': patch
---
Both `EntityProvider` and `AsyncEntityProvider` contexts now wrap all children with an `AnalyticsContext` containing the corresponding `entityRef`; this opens up the possibility for all events underneath these contexts to be associated with and aggregated by the corresponding entity.
+5 -5
View File
@@ -301,11 +301,11 @@ it's important to keep each of these levels of detail disaggregated.
automatically as part of the `extension` in which the `filter` event was
captured).
- On the flip side, when adding `attributes` to an event, look at existing
events and see if the data you are capturing matches the intention, type, or
even the content of _their_ `attributes`. For instance, it may be common for
events that involve the Catalog to add details like entity `name`, `kind`,
and/or `namespace` as `attributes`. Using the same keys in your event will
- On the flip side, when adding `attributes` to or `context` around an event,
look at existing events and see if the data you are capturing matches the
intention, type, or even the content of _their_ `attributes` or `context`.
For instance, it's common for events that involve the Catalog to include an
`entityRef` contextual key. Using the same keys and values in your event will
ensure that events instrumented across plugins can easily be aggregated.
### Unit Testing Event Capture
@@ -23,6 +23,11 @@ import {
AsyncEntityProvider,
} from './useEntity';
import { Entity } from '@backstage/catalog-model';
import { analyticsApiRef, useAnalytics } from '@backstage/core-plugin-api';
import { MockAnalyticsApi, TestApiRegistry } from '@backstage/test-utils';
import { ApiProvider } from '@backstage/core-app-api';
const entity = { metadata: { name: 'my-entity' }, kind: 'MyKind' } as Entity;
describe('useEntity', () => {
it('should throw if no entity is provided', async () => {
@@ -34,7 +39,6 @@ describe('useEntity', () => {
});
it('should provide an entity', async () => {
const entity = { kind: 'MyEntity' } as Entity;
const { result } = renderHook(() => useEntity(), {
wrapper: ({ children }) => (
<EntityProvider entity={entity} children={children} />
@@ -43,6 +47,24 @@ describe('useEntity', () => {
expect(result.current.entity).toBe(entity);
});
it('should provide entityRef analytics context', () => {
const analyticsSpy = new MockAnalyticsApi();
const apis = TestApiRegistry.from([analyticsApiRef, analyticsSpy]);
const { result } = renderHook(() => useAnalytics(), {
wrapper: ({ children }) => (
<ApiProvider apis={apis}>
<EntityProvider entity={entity} children={children} />
</ApiProvider>
),
});
result.current.captureEvent('test', 'value');
expect(analyticsSpy.getEvents()[0]).toMatchObject({
context: { entityRef: 'mykind:default/my-entity' },
});
});
});
describe('useAsyncEntity', () => {
@@ -60,7 +82,6 @@ describe('useAsyncEntity', () => {
});
it('should provide an entity', async () => {
const entity = { kind: 'MyEntity' } as Entity;
const refresh = () => {};
const { result } = renderHook(() => useAsyncEntity(), {
wrapper: ({ children }) => (
@@ -96,4 +117,43 @@ describe('useAsyncEntity', () => {
expect(result.current.error).toBe(error);
expect(result.current.refresh).toBe(undefined);
});
it('should provide entityRef analytics context', () => {
const analyticsSpy = new MockAnalyticsApi();
const apis = TestApiRegistry.from([analyticsApiRef, analyticsSpy]);
const { result } = renderHook(() => useAnalytics(), {
wrapper: ({ children }) => (
<ApiProvider apis={apis}>
<AsyncEntityProvider
loading={false}
entity={entity}
refresh={() => {}}
children={children}
/>
</ApiProvider>
),
});
result.current.captureEvent('test', 'value');
expect(analyticsSpy.getEvents()[0]).toMatchObject({
context: { entityRef: 'mykind:default/my-entity' },
});
});
it('should omit entityRef analytics context', () => {
const analyticsSpy = new MockAnalyticsApi();
const apis = TestApiRegistry.from([analyticsApiRef, analyticsSpy]);
const { result } = renderHook(() => useAnalytics(), {
wrapper: ({ children }) => (
<ApiProvider apis={apis}>
<AsyncEntityProvider loading={false} children={children} />
</ApiProvider>
),
});
result.current.captureEvent('test', 'value');
expect(analyticsSpy.getEvents()[0].context).not.toHaveProperty('entityRef');
});
});
@@ -13,7 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Entity } from '@backstage/catalog-model';
import { Entity, stringifyEntityRef } from '@backstage/catalog-model';
import { AnalyticsContext } from '@backstage/core-plugin-api';
import {
createVersionedContext,
createVersionedValueMap,
@@ -66,7 +67,13 @@ export const AsyncEntityProvider = ({
// consumers might be doing things like `useContext(EntityContext)`
return (
<NewEntityContext.Provider value={createVersionedValueMap({ 1: value })}>
{children}
<AnalyticsContext
attributes={{
...(entity ? { entityRef: stringifyEntityRef(entity) } : undefined),
}}
>
{children}
</AnalyticsContext>
</NewEntityContext.Provider>
);
};