diff --git a/.changeset/analyze-software-exploration.md b/.changeset/analyze-software-exploration.md new file mode 100644 index 0000000000..f535f60127 --- /dev/null +++ b/.changeset/analyze-software-exploration.md @@ -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. diff --git a/docs/plugins/analytics.md b/docs/plugins/analytics.md index ccb4e5c5a7..0c2173de5c 100644 --- a/docs/plugins/analytics.md +++ b/docs/plugins/analytics.md @@ -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 diff --git a/plugins/catalog-react/src/hooks/useEntity.test.tsx b/plugins/catalog-react/src/hooks/useEntity.test.tsx index 5e86a94180..3a1e5010d3 100644 --- a/plugins/catalog-react/src/hooks/useEntity.test.tsx +++ b/plugins/catalog-react/src/hooks/useEntity.test.tsx @@ -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 }) => ( @@ -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 }) => ( + + + + ), + }); + + 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 }) => ( + + {}} + children={children} + /> + + ), + }); + + 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 }) => ( + + + + ), + }); + + result.current.captureEvent('test', 'value'); + + expect(analyticsSpy.getEvents()[0].context).not.toHaveProperty('entityRef'); + }); }); diff --git a/plugins/catalog-react/src/hooks/useEntity.tsx b/plugins/catalog-react/src/hooks/useEntity.tsx index f654cff777..84b7883c8a 100644 --- a/plugins/catalog-react/src/hooks/useEntity.tsx +++ b/plugins/catalog-react/src/hooks/useEntity.tsx @@ -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 ( - {children} + + {children} + ); };