Angular에서 OpenReplay 사용하기

Angular 애플리케이션에서 트래커를 동작시키는 방법을 알아봅니다.

Angular에서 OpenReplay 사용하기

중복 실행과 불필요한 검사를 방지하려면 트래커를 Angular의 Zone.js 훅 밖에서 반드시 실행해야 한다는 점에 유의하세요.

다음은 트래커 설정의 간단한 예시입니다.

import { Injectable, NgZone } from '@angular/core';
import Tracker from '@openreplay/tracker';
import trackerAssist from '@openreplay/tracker-assist';

@Injectable({
  providedIn: 'root',
})
export class OpenReplayService {
  public tracker?: Tracker | null;

  constructor(private zone: NgZone) {
    this.zone.runOutsideAngular(() => {
      this.tracker = new Tracker({
        projectKey: 'abc123',
        ingestPoint: 'https://someurl/',
      });

      this.tracker.use(
        trackerAssist({
          confirmText: `You have an incoming call from <Company> Support. Do you want to answer?`,
        })
      );
    });
  }

  public async start() {
    this.zone.runOutsideAngular(() => {
      if (this.tracker) {
        return this.tracker.start();
      } else {
        return {
          sessionID: null,
          sessionToken: null,
          userUUID: null,
        };
      }
    });
  }

  public setUserData(user: { id: string }): void {
    this.zone.runOutsideAngular(() => {
      if (this.tracker && user.id) {
        this.tracker.setUserID(String(user.id));
      }
    });
  }
}

Angular 애플리케이션에서 전송되는 요청을 추적하려는 경우, OpenReplay는 Fetch나 Axios처럼 전용 플러그인을 제공하지 않습니다.

그래도 HTTPInterceptor를 사용하면 원하는 정보를 담아 요청을 추적하도록 설정할 수 있습니다.

이 튜토리얼에서는 HTTPClient가 보내는 요청과 응답을 모두 기록할 수 있는 HTTPInterceptor를 만드는 방법을 보여 드리겠습니다.

인터셉터는 앱 코드에 주입할 수 있는 특수한 유형의 객체로, Angular의 기본 HTTP 클라이언트가 보내는 모든 요청을 가로채고 그 응답을 캡처합니다.

이 로직을 이용해 OpenReplay의 커스텀 이벤트를 활용합니다. 커스텀 이벤트를 사용하면 세션 안에 캡처하고 싶은 이벤트를 무엇이든 보낼 수 있으므로, 다른 환경에서 Fetch나 Axios 플러그인이 하는 일을 똑같이 구현할 수 있습니다.

인터셉터 코드는 다음과 같습니다.

import { Injectable } from '@angular/core';
import {
  HttpInterceptor,
  HttpRequest,
  HttpHandler,
  HttpEvent,
  HttpResponse,
} from '@angular/common/http';

import { Observable } from 'rxjs';
import { filter, map } from 'rxjs/operators';
import { ReplaySessionService } from '../replay-session.service';

@Injectable({providedIn: 'root'})
export class HttpConfigInterceptor implements HttpInterceptor {
  constructor(
    private replaySessionService: ReplaySessionService,
  ) { }
  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    
		//This function will be called with the response a few lines below
		const handleResponse = (request: HttpRequest<any>, response: HttpResponse<any>, event: string) => {
	     //we forward our data to the service, which will create the custom event and send it
			this.replaySessionService.sendEventToReplaySession(event, { request, response })
    }
    return next.handle(request).pipe(
      //filter out events that aren't http reponses
      filter( (event: any) => event instanceof HttpResponse),
      map( (resp: HttpResponse<any>) => { //for each response, call handleResponse
        handleResponse(request, resp, `${request.url}`)
        return resp
      }),
      map((event: HttpEvent<any>) => {
        return event;
      })
    );
  }
}

리플레이 서비스는 잠시 뒤에 살펴보겠지만, 지금은 이미 있다고 가정하세요. 이 파일은 app 폴더 안에 저장합니다.

그런 다음 app.module 파일을 편집하여 @ngModule 지시문 안에 다음을 추가합니다.

providers: [
    {provide: HTTP_INTERCEPTORS, useClass: HttpConfigInterceptor, multi: true}
  ]

그러면 app.module 파일은 다음과 비슷한 모습이 됩니다.

import { NgModule } from '@angular/core';

/*
imports...
*/
import { HttpConfigInterceptor } from './interceptor/index';

@NgModule({
  imports: [
   /*...*/
  ],
  providers: [
    {provide: HTTP_INTERCEPTORS, useClass: HttpConfigInterceptor, multi: true}
  ],
  declarations: [
    /*...*/
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }

이제 애플리케이션이 수행하는 모든 HTTP 요청에 인터셉터가 주입됩니다.

이제 실제 세션 리플레이 서비스를 살펴보겠습니다.

SessionReplayService 만들기

Section titled SessionReplayService 만들기

먼저 다음 명령으로 새 서비스를 추가합니다.

$ ng generate service replay-session

이 명령은 앱 루트에 replay-session.service.ts라는 새 Angular 서비스를 생성합니다.

해당 파일의 내용은 다음과 같습니다.

import { Injectable } from '@angular/core';
import {
  HttpInterceptor,
  HttpRequest,
  HttpHandler,
  HttpResponse,
} from '@angular/common/http';
import OpenReplay from '@openreplay/tracker'

type ReqRespType = {
  request: HttpRequest<any>,
  response: HttpResponse<any>
}

@Injectable({
  providedIn: 'root'
})
export class ReplaySessionService {
  tracker: OpenReplay|null = null

  constructor() {

    this.tracker = new OpenReplay({
        projectKey: "<YOUR PROJECT KEY>",
    })
		//you can set up any other OR plugins here as well

    this.tracker.start()
   }

  sendEventToReplaySession(event: string, params: ReqRespType): void {
    const {request, response} = params

    this.tracker?.event(event + "[request]", {
      method: request.method,
      url: request.url,
      params: request.params
    })
    this.tracker?.event(event + "[response]", {
      body: response.body,
      status: response.status,
      headers: response.headers
    })
  }
}

이 클래스의 생성자는 서비스이므로 한 번만 호출되며, 트래커를 인스턴스화하고 시작하는 역할을 합니다.

그다음 sendEventToReplaySession 메서드에서 event 메서드를 사용해 두 개의 커스텀 이벤트를 전송합니다.

인터셉터 클래스로 돌아가 보면, 이 메서드가 받는 첫 번째 매개변수인 “event”가 사실은 URL이라는 것을 알 수 있습니다. 그래서 어디에 무엇이 기록되는지 구분할 수 있도록 URL 뒤에 “[response]”와 “[request]”를 붙였습니다. 그런 다음 각 이벤트의 페이로드를 만들어 저장하고 싶은 정보만 담습니다.

사실 이 코드를 활용하면 event 메서드를 호출하기 전에, 리플레이에 보이지 않았으면 하는 요청이나 응답의 필드를 정제할 수도 있습니다.

트래커가 적용된 Angular 기반 애플리케이션의 전체 소스 코드는 이 저장소에서 확인할 수 있습니다.

Angular 프로젝트에 트래커를 설정하는 중 문제가 있다면 Slack 커뮤니티로 문의하여 개발자에게 직접 질문하세요!