플러그인 애널리틱스
레거시 문서
출처: 문서
본문
레거시 문서
이 섹션은 레거시 플러그인 문서의 일부예요. 새 프론트엔드 시스템 버전은 Plugin Analytics를 참고하세요. 여기 설명된 개념과 이벤트는 이전 및 새 프론트엔드 시스템 모두에 적용돼요.
Backstage 인스턴스를 설정하고 유지하며 반복하는 것은 큰 투자가 될 수 있어요. 이 투자에 대한 수익을 측정하는 데 도움이 되도록 Backstage에는 이벤트 기반 Analytics API가 포함되어 있어요. 이 API는 앱 통합자에게 선택한 애널리틱스 도구에서 Backstage 사용량을 수집·분석할 수 있는 유연성을 부여하고, 플러그인 개발자에게는 핵심 사용자 상호작용을 계측하기 위한 표준 인터페이스를 제공해요.
개념
- 이벤트는 최소한
action(예:click)과subject(예:클릭된 것)으로 구성돼요. - 속성(attributes)은 이벤트별로 제공될 수 있는 추가 차원 데이터(키/값 쌍 형태)를 나타내요. 위 예시를 이어서, 사용자가 클릭해서 이동한 URL은
{ "to": "/a/page" }처럼 보일 수 있어요. - 컨텍스트(context)는 이벤트가 발생한 더 넓은 맥락을 나타내요. 기본적으로
pluginId,extension,routeRef같은 정보가 제공돼요.
이 이벤트 구성은 다양한 세부 수준에서 분석을 가능하게 하려는 것을 목표로 해요. 매우 세분화된 질문(예: "특정 라우트에서 가장 많이 클릭된 것은 무엇인가")부터 매우 높은 수준의 질문(예: "내 Backstage 인스턴스에서 가장 많이 사용된 플러그인은 무엇인가")까지 답할 수 있게 해줘요.
지원되는 애널리틱스 도구
이 이벤트를 소비해 애널리틱스 도구로 전달하는 데 필요한 모든 것은 AnalyticsApi의 구체적인 구현이지만, 일반적인 통합은 패키지화되어 플러그인으로 제공돼요. 아래에서 선택한 애널리틱스 도구를 찾으세요.
| | Analytics Tool | Support Status | | | Google Analytics 4 | Yes ✅ | | | New Relic Browser | Community ✅ | | | Matomo | Community ✅ | | | Quantum Metric | Community ✅ | | | Generic HTTP | Community ✅ |
통합을 제안하려면 조직이 사용하는 애널리틱스 도구에 대한 이슈를 열어 주세요. 또는 Writing Integrations로 이동해 직접 통합을 기여하는 방법을 배우세요!
주요 이벤트
다음 표는 설치한 플러그인에 따라 캡처될 수 있는 이벤트를 요약해요.
| | Action | Subject | Other Notes |
| | navigate | 이동한 페이지의 URL. | 라우트 위치가 변경될 때 즉시 발생(연결된 플러그인/라우트 데이터가 모호하면, 플러그인/라우트 데이터가 알려진 후, 다음 이벤트 또는 문서 언로드 직전에 발생). 현재 라우트의 파라미터가 속성으로 포함돼요. |
| | click | 클릭된 링크의 텍스트. | to 속성은 클릭해서 이동한 URL을 나타내요. |
| | create | 생성되는 소프트웨어의 name. 주어진 Software Template이 name 속성을 요청하지 않으면 문자열 new {templateName}이 대신 사용돼요. | 컨텍스트는 템플릿의 ref(예: template:default/template-name)로 설정된 entityRef를 담아요. value는 템플릿 실행으로 절약된 분 수를 나타내요(가능하면 템플릿의 backstage.io/time-saved 어노테이션 기준). |
| | search | 검색 바 구성 요소에 입력된 검색어. | 컨텍스트는 검색을 제한하는 types를 나타내는 searchTypes를 담아요. value는 쿼리 대한 총 검색 결과 수. 권한 프레임워크를 사용 중이면 보이지 않을 수 있어요. |
| | discover | 클릭된 검색 결과의 제목. | value는 결과 순위. to 속성도 제공돼요. |
| | not-found | not found 페이지가 된 리소스의 경로. | 적어도 TechDocs가 발생시켜요. |
캡처되기를 원하는 이벤트가 있다면, 보고 싶은 이벤트와 그것이 답하는 데 도움이 될 질문을 설명하는 이슈를 열어 주세요. 또는 Capturing Events로 이동해 직접 계측을 기여하는 방법을 배우세요!
OSS 플러그인 메인테이너: 위 표에 이벤트를 자유롭게 문서화하세요.
통합 작성
애널리틱스 이벤트 전달은 Backstage utility API로 구현돼요. 오류나 SCM 인증에 대해 사용자 지정 API 구현을 제공하는 것처럼, 애널리틱스에 대해서도 제공할 수 있어요.
제공된 API는 AnalyticsEvent 객체를 받는 단일 메서드 captureEvent만 제공하면 돼요.
import { analyticsApiRef, AnalyticsEvent, AnyApiFactory, createApiFactory,} from '@backstage/core-plugin-api';export const apis: AnyApiFactory[] = [ createApiFactory(analyticsApiRef, { captureEvent: (event: AnalyticsEvent) => { window._AcmeAnalyticsQ.push(event); }, }),];// Or, when building for the new frontend system:import { AnalyticsImplementationBlueprint } from '@backstage/frontend-plugin-api';export const acmeAnalyticsImplementation = AnalyticsImplementationBlueprint.make({ name: 'acme', params: define => define({ deps: {}, factory() { return { captureEvent: event => { window._AcmeAnalyticsQ.push(event); }, }; }, }), });
실제로는 인스턴스화 로직을 캡슐화하고 구성에서 일부 세부 정보를 가져오고 싶을 거예요. 더 완전한 예시는 이렇게 보일 수 있어요.
import { AnalyticsApi, analyticsApiRef, AnalyticsEvent, AnyApiFactory, configApiRef, createApiFactory,} from '@backstage/core-plugin-api';import { AcmeAnalytics } from 'acme-analytics';class AcmeAnalytics implements AnalyticsApi { private constructor(accountId: number) { AcmeAnalytics.init(accountId); } static fromConfig(config) { const accountId = config.getString('app.analytics.acme.id'); return new AcmeAnalytics(accountId); } captureEvent(event: AnalyticsEvent) { const { action, ...rest } = event; AcmeAnalytics.send(action, rest); }}export const apis: AnyApiFactory[] = [ createApiFactory({ api: analyticsApiRef, deps: { configApi: configApiRef }, factory: ({ configApi }) => AcmeAnalytics.fromConfig(configApi), }),];// Or, when building for the new frontend system:import { AnalyticsImplementationBlueprint } from '@backstage/frontend-plugin-api';export const acmeAnalyticsImplementation = AnalyticsImplementationBlueprint.make({ name: 'acme', params: define => define({ deps: { configApi: configApiRef }, factory: ({ configApi }) => AcmeAnalytics.fromConfig(configApi), }), });
애널리틱스 서비스(내부 도구가 아닌)와 통합하고 있다면, API 구현을 플러그인으로 기여하는 것을 고려해 보세요!
관례상 이런 패키지는 @backstage/analytics-module-[name]로 이름을 지어야 하며, 모든 구성은 app.analytics.[name] 아래에 키를 둬야 해요.
사용자 신원 처리
통합하는 애널리틱스 플랫폼에 사용자 신원의 일급 개념이 있다면, 다음 관례를 따라 이를 지원하도록 선택할 수 있어요.
- 옵션 중 하나로
identityApi와 함께fromConfig정적 메서드에서 구현이 인스턴스화되도록 허용하세요. identityApi의getBackstageIdentity()메서드가 해석하는userEntityRef를 애널리틱스 플랫폼에 보내는 사용자 ID의 기반으로 사용하세요.
예를 들어:
import { AnalyticsApi, analyticsApiRef, AnyApiFactory, configApiRef, createApiFactory, identityApiRef, IdentityApi,} from '@backstage/core-plugin-api';// Implementation that optionally initializes with a userId.class AcmeAnalytics implements AnalyticsApi { private constructor(accountId: number, identityApi?: IdentityApi) { if (identityApi) { identityApi.getBackstageIdentity().then(identity => { AcmeAnalytics.init(accountId, { userId: identity.userEntityRef, }); }); } else { AcmeAnalytics.init(accountId); } } static fromConfig(config, options) { const accountId = config.getString('app.analytics.acme.id'); return new AcmeAnalytics(accountId, options.identityApi); }}// Your implementation should be instantiated like this:export const apis: AnyApiFactory[] = [ createApiFactory({ api: analyticsApiRef, deps: { configApi: configApiRef, identityApi: identityApiRef }, factory: ({ configApi, identityApi }) => AcmeAnalytics.fromConfig(configApi, { identityApi, }), }),];
이벤트 캡처
구성 요소에서 이벤트를 계측하려면 @backstage/core-plugin-api가 제공하는 useAnalytics() 훅을 사용해 애널리틱스 추적기를 검색하는 것으로 시작하세요. 추적기에는 action과 subject를 인자로 받는 captureEvent 메서드가 있어요.
import { useAnalytics } from '@backstage/core-plugin-api';const analytics = useAnalytics();analytics.captureEvent('deploy', serviceName);
추가 속성 제공
추가 차원 attributes와 숫자 value는 이벤트와 관련이 있다면 세 번째 options 인자에 제공할 수 있어요.
analytics.captureEvent('merge', pullRequestName, { value: pullRequestAgeInMinutes, attributes: { org, repo, },});
위 예시에서 다음과 유사한 객체의 이벤트가 캡처될 거예요.
{ "action": "merge", "subject": "Name of Pull Request", "value": 60, "attributes": { "org": "some-org", "repo": "some-repo" }}
이벤트에 컨텍스트 제공
attributes 옵션은 계측 중인 구성 요소 안에서 사용할 수 있는 세부 정보를 캡처하는 데 좋아요. React 트리 더 위에서만 사용 가능한 메타데이터를 캡처하거나, 앱 통합자가 공통 값으로 서로 다른 이벤트를 집계하는 데 도움이 되도록 <AnalyticsContext>를 사용하세요.
import { AnalyticsContext, useAnalytics } from '@backstage/core-plugin-api';const MyComponent = ({ value }) => { const analytics = useAnalytics(); const handleClick = () => analytics.captureEvent('check', value); return <SomeThing value={value} onClick={handleClick} />;};const MyWrapper = () => { return ( <AnalyticsContext attributes={{ segment: 'xyz' }}> <MyComponent value={'Some Value'} /> </AnalyticsContext> );};
위 예시에서 <SomeThing />을 클릭하면 다음과 유사한 애널리틱스 이벤트가 발생해요.
{ "action": "check", "subject": "Some Value", "context": { "segment": "xyz" }}
위 예시의 간결함을 위해 Backstage 코어가 제공하는 컨텍스트 키(pluginId, extension, routeRef)는 생략했어요. 실제로 그 세부 정보는 여러분이 제공하는 추가 컨텍스트와 함께 포함될 거예요.
애널리틱스 컨텍스트는 중첩될 수 있으며, 그 값은 React 트리 아래로 병합되어 키가 덮어쓰여질 수 있어요.
이벤트 명명 고려 사항
이벤트는 다양한 세부 수준에서 분석을 가능하게 하기 위해 구성 부분으로 나뉘어요. 분석 시점에 이 유연성을 유지하려면 각 세부 수준을 분리해 두는 것이 중요해요.
- 지나치게 특정한
action을 제공하지 마세요. 예를 들어filterEntityTable대신filter를 액션으로 사용하고,EntityTable이 이벤트의context의 일부로 지정되도록 허용하는 것을 고려하세요(filter이벤트가 캡처된extension의 일부로 자동일 가능성이 가장 높아요). - 반대로, 이벤트에
attributes를 추가하거나context를 추가할 때는 기존 이벤트를 보고 캡처하는 데이터가 그것들의attributes나context의 의도, 타입, 심지어 내용과 일치하는지 살펴보세요. 예를 들어 Catalog와 관련된 이벤트는entityRef컨텍스트 키를 포함하는 것이 흔해요. 이벤트에서 같은 키와 값을 사용하면 플러그인에 걸쳐 계측된 이벤트가 쉽게 집계되도록 보장해요.
이벤트 캡처 단위 테스트
@backstage/test-utils 패키지에는 단위 테스트에서 캡처된 애널리틱스 이벤트를 스파이하고 검증하는 데 사용할 수 있는 MockAnalyticsApi 구현이 포함돼요.
이렇게 사용해요:
import { render, fireEvent, waitFor } from '@testing-library/react';import { analyticsApiRef } from '@backstage/core-plugin-api';import { MockAnalyticsApi, TestApiProvider, wrapInTestApp,} from '@backstage/test-utils';describe('SomeComponent', () => { it('should capture event on click', () => { // Use the Mock Analytics API to spy on event captures. const apiSpy = new MockAnalyticsApi(); // Render the component being tested const { getByText } = render( wrapInTestApp( <TestApiProvider apis={[[analyticsApiRef, apiSpy]]}> <SomeComponentUnderTest /> </TestApiProvider>, ), ); // Fire the event that triggers event capture. fireEvent.click(getByText('some component text')); // Assert that the event was captured with the expected data. await waitFor(() => { expect(apiSpy.getEvents()[0]).toMatchObject({ action: 'expected action', subject: 'expected subject', attributes: { foo: 'bar', }, }); }); });});