AnalyticsBlueprint -> AnalyticsImplementationBlueprint

Signed-off-by: Eric Peterson <ericpeterson@spotify.com>
This commit is contained in:
Eric Peterson
2025-07-30 09:20:55 +02:00
parent 93b5e38f8b
commit 23676c1add
10 changed files with 75 additions and 69 deletions
+3 -3
View File
@@ -2,12 +2,12 @@
'@backstage/frontend-plugin-api': patch
---
Plugins should now use the new `AnalyticsBlueprint` to define and provide concrete analytics implementations. For example:
Plugins should now use the new `AnalyticsImplementationBlueprint` to define and provide concrete analytics implementations. For example:
```ts
import { AnalyticsBlueprint } from '@backstage/frontend-plugin-api';
import { AnalyticsImplementationBlueprint } from '@backstage/frontend-plugin-api';
const AcmeAnalytics = AnalyticsBlueprint.make({
const AcmeAnalytics = AnalyticsImplementationBlueprint.make({
name: 'acme-analytics',
params: define =>
define({
+1 -1
View File
@@ -2,4 +2,4 @@
'@backstage/plugin-app': patch
---
The default implementation of the Analytics API now collects and instantiates analytics implementations exposed via `AnalyticsBlueprint` extensions. If no such extensions are discovered, the API continues to do nothing with analytics events fired within Backstage. If multiple such extensions are discovered, every discovered implementation automatically receives analytics events.
The default implementation of the Analytics API now collects and instantiates analytics implementations exposed via `AnalyticsImplementationBlueprint` extensions. If no such extensions are discovered, the API continues to do nothing with analytics events fired within Backstage. If multiple such extensions are discovered, every discovered implementation automatically receives analytics events.
+26 -24
View File
@@ -103,22 +103,23 @@ export const apis: AnyApiFactory[] = [
];
// Or, when building for the new frontend system:
import { AnalyticsBlueprint } from '@backstage/frontend-plugin-api';
import { AnalyticsImplementationBlueprint } from '@backstage/frontend-plugin-api';
export const acmeAnalyticsImplementation = AnalyticsBlueprint.make({
name: 'acme',
params: define =>
define({
deps: {},
factory() {
return {
captureEvent: event => {
window._AcmeAnalyticsQ.push(event);
},
};
},
}),
});
export const acmeAnalyticsImplementation =
AnalyticsImplementationBlueprint.make({
name: 'acme',
params: define =>
define({
deps: {},
factory() {
return {
captureEvent: event => {
window._AcmeAnalyticsQ.push(event);
},
};
},
}),
});
```
In reality, you would likely want to encapsulate instantiation logic and pull
@@ -160,16 +161,17 @@ export const apis: AnyApiFactory[] = [
];
// Or, when building for the new frontend system:
import { AnalyticsBlueprint } from '@backstage/frontend-plugin-api';
import { AnalyticsImplementationBlueprint } from '@backstage/frontend-plugin-api';
export const acmeAnalyticsImplementation = AnalyticsBlueprint.make({
name: 'acme',
params: define =>
define({
deps: { configApi: configApiRef },
factory: ({ configApi }) => AcmeAnalytics.fromConfig(configApi),
}),
});
export const acmeAnalyticsImplementation =
AnalyticsImplementationBlueprint.make({
name: 'acme',
params: define =>
define({
deps: { configApi: configApiRef },
factory: ({ configApi }) => AcmeAnalytics.fromConfig(configApi),
}),
});
```
If you are integrating with an analytics service (as opposed to an internal
+24 -24
View File
@@ -107,30 +107,6 @@ export type AnalyticsApi = {
// @public
export const analyticsApiRef: ApiRef<AnalyticsApi>;
// @public
export const AnalyticsBlueprint: ExtensionBlueprint<{
kind: 'analytics';
name: undefined;
params: <TDeps extends { [name in string]: unknown }>(
params: AnalyticsImplementationFactory<TDeps>,
) => ExtensionBlueprintParams<AnalyticsImplementationFactory<{}>>;
output: ConfigurableExtensionDataRef<
AnalyticsImplementationFactory<{}>,
'core.analytics.factory',
{}
>;
inputs: {};
config: {};
configInput: {};
dataRefs: {
factory: ConfigurableExtensionDataRef<
AnalyticsImplementationFactory<{}>,
'core.analytics.factory',
{}
>;
};
}>;
// @public
export const AnalyticsContext: (options: {
attributes: Partial<AnalyticsContextValue>;
@@ -164,6 +140,30 @@ export type AnalyticsImplementation = {
captureEvent(event: AnalyticsEvent): void;
};
// @public
export const AnalyticsImplementationBlueprint: ExtensionBlueprint<{
kind: 'analytics';
name: undefined;
params: <TDeps extends { [name in string]: unknown }>(
params: AnalyticsImplementationFactory<TDeps>,
) => ExtensionBlueprintParams<AnalyticsImplementationFactory<{}>>;
output: ConfigurableExtensionDataRef<
AnalyticsImplementationFactory<{}>,
'core.analytics.factory',
{}
>;
inputs: {};
config: {};
configInput: {};
dataRefs: {
factory: ConfigurableExtensionDataRef<
AnalyticsImplementationFactory<{}>,
'core.analytics.factory',
{}
>;
};
}>;
// @public (undocumented)
export type AnalyticsImplementationFactory<
Deps extends {
@@ -16,7 +16,7 @@
import { ApiRef, createApiRef } from '@backstage/core-plugin-api';
import { AnalyticsContextValue } from '../../analytics/types';
import type { AnalyticsBlueprint } from '../../blueprints/';
import type { AnalyticsImplementationBlueprint } from '../../blueprints/';
/**
* Represents an event worth tracking in an analytics system that could inform
@@ -149,8 +149,8 @@ export type AnalyticsApi = {
*
* @remarks
*
* To define a concrete Analytics Implementation, use {@link AnalyticsBlueprint}
* instead.
* To define a concrete Analytics Implementation, use
* {@link AnalyticsImplementationBlueprint} instead.
*
* @public
*/
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { AnalyticsBlueprint } from './AnalyticsBlueprint';
import { AnalyticsImplementationBlueprint } from './AnalyticsImplementationBlueprint';
describe('AnalyticsBlueprint', () => {
it('should create an extension with sensible defaults', () => {
@@ -23,7 +23,7 @@ describe('AnalyticsBlueprint', () => {
factory: () => ({ captureEvent: () => {} }),
};
const extension = AnalyticsBlueprint.make({
const extension = AnalyticsImplementationBlueprint.make({
params: define => define(factory),
name: 'test',
});
@@ -39,7 +39,7 @@ const factoryDataRef =
*
* @public
*/
export const AnalyticsBlueprint = createExtensionBlueprint({
export const AnalyticsImplementationBlueprint = createExtensionBlueprint({
kind: 'analytics',
attachTo: [{ id: 'api:app/analytics', input: 'analyticsImplementations' }],
output: [factoryDataRef],
@@ -15,9 +15,9 @@
*/
export {
AnalyticsBlueprint,
AnalyticsImplementationBlueprint,
type AnalyticsImplementationFactory,
} from './AnalyticsBlueprint';
} from './AnalyticsImplementationBlueprint';
export { ApiBlueprint } from './ApiBlueprint';
export { AppRootElementBlueprint } from './AppRootElementBlueprint';
export { AppRootWrapperBlueprint } from './AppRootWrapperBlueprint';
@@ -16,7 +16,7 @@
import { createExtensionTester } from '@backstage/frontend-test-utils';
import { AnalyticsApi } from './AnalyticsApi';
import {
AnalyticsBlueprint,
AnalyticsImplementationBlueprint,
AnalyticsImplementation,
ApiBlueprint,
configApiRef,
@@ -38,10 +38,10 @@ describe('AnalyticsApi', () => {
const MockImplementation1 = createExtension({
name: 'mock-implementation-1',
attachTo: { id: 'api:analytics', input: 'analyticsImplementations' },
output: [AnalyticsBlueprint.dataRefs.factory],
output: [AnalyticsImplementationBlueprint.dataRefs.factory],
factory() {
return [
AnalyticsBlueprint.dataRefs.factory({
AnalyticsImplementationBlueprint.dataRefs.factory({
deps: { configApi: configApiRef },
factory: deps => ({
captureEvent: event => captureEventSpy1(event, deps),
@@ -54,10 +54,10 @@ describe('AnalyticsApi', () => {
const MockImplementation2 = createExtension({
name: 'mock-implementation-2',
attachTo: { id: 'api:analytics', input: 'analyticsImplementations' },
output: [AnalyticsBlueprint.dataRefs.factory],
output: [AnalyticsImplementationBlueprint.dataRefs.factory],
factory() {
return [
AnalyticsBlueprint.dataRefs.factory({
AnalyticsImplementationBlueprint.dataRefs.factory({
deps: { config: configApiRef, identityApi: identityApiRef },
factory: deps => ({
captureEvent: event => captureEventSpy2(event, deps),
+8 -4
View File
@@ -16,7 +16,7 @@
import {
analyticsApiRef,
AnalyticsBlueprint,
AnalyticsImplementationBlueprint,
ApiBlueprint,
ApiRef,
createExtensionInput,
@@ -26,7 +26,7 @@ export const AnalyticsApi = ApiBlueprint.makeWithOverrides({
name: 'analytics',
inputs: {
analyticsImplementations: createExtensionInput([
AnalyticsBlueprint.dataRefs.factory,
AnalyticsImplementationBlueprint.dataRefs.factory,
]),
},
factory(originalFactory, { inputs }) {
@@ -35,7 +35,9 @@ export const AnalyticsApi = ApiBlueprint.makeWithOverrides({
// if they were its own deps.
const aggregatedDeps = inputs.analyticsImplementations
.flatMap<ApiRef<unknown>>(impls =>
Object.values(impls.get(AnalyticsBlueprint.dataRefs.factory).deps),
Object.values(
impls.get(AnalyticsImplementationBlueprint.dataRefs.factory).deps,
),
)
.reduce<{ [x: string]: ApiRef<unknown> }>((accum, ref) => {
accum[ref.id] = ref;
@@ -48,7 +50,9 @@ export const AnalyticsApi = ApiBlueprint.makeWithOverrides({
deps: aggregatedDeps,
factory: analyticsApiDeps => {
const actualApis = inputs.analyticsImplementations
.map(impl => impl.get(AnalyticsBlueprint.dataRefs.factory))
.map(impl =>
impl.get(AnalyticsImplementationBlueprint.dataRefs.factory),
)
.map(({ factory, deps }) =>
factory(
// Reconstruct a deps argument to pass to this analytics