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}
+
);
};