imqa.document

사용자 이벤트 추적

@imqa/user-event

NPM Version NPM Last Update NPM Unpacked Size npm package minimized gzipped size NPM Downloads NPM Type Definitions

개요

사용자 이벤트 추적은 웹 애플리케이션에서 발생하는 특정 비즈니스 로직이나 사용자 행동의 시작과 완료를 측정하여 전체 duration을 계측하는 기능을 제공합니다. 자동 계측으로 포착하기 어려운 커스텀 사용자 여정이나 복잡한 워크플로우를 추적하여 사용자 경험과 비즈니스 프로세스를 심층적으로 분석할 수 있습니다.

주요 기능

커스텀 이벤트 추적

  • 비즈니스 로직의 시작과 완료 지점 명시적 지정
  • 전체 duration 자동 계산 및 기록
  • 고유한 이벤트 ID를 통한 이벤트 관리

컨텍스트 그룹화

  • 이벤트 실행 중 발생하는 모든 계측(fetch, console, exception 등)을 자동으로 그룹화
  • 사용자 이벤트와 연관된 모든 활동을 단일 컨텍스트로 추적
  • 분산 추적을 통한 전체 플로우 가시성 확보

유연한 이벤트 관리

  • 이벤트 시작, 완료, 취소 기능
  • 이벤트별 커스텀 속성 추가
  • 활성 이벤트 상태 조회

성능 분석

  • 사용자 여정의 각 단계별 소요 시간 측정
  • 비즈니스 프로세스의 병목 구간 식별
  • 사용자 행동 패턴 분석

계측 범위

사용자 이벤트 추적은 다음과 같은 시나리오에서 활용됩니다:

  • 폼 제출 워크플로우: 입력부터 검증, 제출까지의 전체 프로세스
  • 파일 업로드: 파일 선택, 업로드, 완료까지의 과정
  • 검색 및 필터링: 검색어 입력부터 결과 표시까지
  • 장바구니 및 결제: 상품 선택부터 결제 완료까지
  • 다단계 마법사: 여러 단계로 구성된 사용자 입력 프로세스
  • 비동기 작업: 긴 실행 시간이 필요한 백그라운드 작업

주요 속성

속성설명
service.nameTelemetry를 생성하는 서비스의 이름
service.version서비스 버전
service.namespace서비스 네임스페이스
deployment.environment.name배포 환경
telemetry.sdk.languageTelemetry SDK의 프로그래밍 언어
telemetry.sdk.nameTelemetry SDK의 이름
telemetry.sdk.versionTelemetry SDK의 버전
process.runtime.name런타임 이름 (예: "browser")
os.name운영체제 이름
os.version운영체제 버전
imqa.browser.device디바이스 타입
imqa.browser.name브라우저 이름
imqa.browser.version브라우저 전체 버전
imqa.browser.version_major브라우저 메이저 버전
service.key서비스 식별 키
imqa.agent.versionIMQA 에이전트 버전
rum.versionRUM (Real User Monitoring) 버전
rum.scriptInstanceRUM 스크립트 인스턴스 식별자
session.id사용자 세션 식별자

인스트루멘테이션 범위

사용자 이벤트 Telemetry는 주로 다음과 같은 인스트루멘테이션 범위를 통해 캡처됩니다:

  • @imqa/user-event: 커스텀 사용자 이벤트 및 비즈니스 로직 추적

API 레퍼런스

userEvent.start()

사용자 이벤트를 시작하고 고유한 이벤트 ID를 반환합니다.

IMQA.userEvent.start(name?: string, attributes?: Attributes): string

매개변수:

  • name (string, optional): 이벤트 이름 (기본값: user-event-[randomId])
  • attributes (Attributes, optional): 이벤트에 추가할 커스텀 속성

반환값:

  • 이벤트의 고유 식별자 (string)

예시:

const eventId = IMQA.userEvent.start('checkout-process', {
  'cart.items': 3,
  'user.type': 'premium'
});

userEvent.end()

지정된 ID의 사용자 이벤트를 완료하고 span을 기록합니다. 전체 duration이 자동으로 계산됩니다.

IMQA.userEvent.end(id: string, attributes?: Attributes): void

매개변수:

  • id (string, required): start()에서 반환된 이벤트 ID
  • attributes (Attributes, optional): 이벤트 완료 시 추가할 속성

예시:

IMQA.userEvent.end(eventId, {
  'checkout.result': 'success',
  'order.id': 'ORD-12345'
});

userEvent.getActive()

현재 활성화된 사용자 이벤트 정보를 반환합니다.

IMQA.userEvent.getActive(): UserEventInstance | undefined

반환값:

  • 활성 이벤트 인스턴스 또는 undefined

예시:

const activeEvent = IMQA.userEvent.getActive();
if (activeEvent) {
  console.log('Active event:', activeEvent);
}

userEvent.cancel()

지정된 ID의 사용자 이벤트를 취소합니다. span을 기록하지 않습니다.

IMQA.userEvent.cancel(id: string): void

매개변수:

  • id (string, required): 취소할 이벤트 ID

예시:

IMQA.userEvent.cancel(eventId);

userEvent.with()

특정 사용자 이벤트의 컨텍스트 내에서 함수를 실행합니다. 해당 함수 실행 중 모든 계측이 지정된 사용자 이벤트 컨텍스트를 사용합니다.

IMQA.userEvent.with<T>(id: string, fn: () => T): T

매개변수:

  • id (string, required): 사용자 이벤트 ID
  • fn (() => T, required): 실행할 함수

반환값:

  • 함수 실행 결과

예시:

const eventId = IMQA.userEvent.start('data-processing');

// 특정 이벤트 컨텍스트에서 비동기 작업 실행
const result = await IMQA.userEvent.with(eventId, async () => {
  // 이 안에서 발생하는 모든 fetch, console 등이 eventId와 연결됩니다
  const data = await fetchData();
  const processed = await processData(data);
  return processed;
});

IMQA.userEvent.end(eventId);

userEvent.withActive()

현재 활성화된 사용자 이벤트의 컨텍스트 내에서 함수를 실행합니다.

IMQA.userEvent.withActive<T>(fn: () => T): T

매개변수:

  • fn (() => T, required): 실행할 함수

반환값:

  • 함수 실행 결과

예시:

const eventId = IMQA.userEvent.start('batch-operation');

// 현재 활성 이벤트 컨텍스트에서 작업 실행
IMQA.userEvent.withActive(() => {
  performTask1();
  performTask2();
  performTask3();
});

IMQA.userEvent.end(eventId);

userEvent.switchTo()

특정 사용자 이벤트로 컨텍스트를 전환합니다. 중첩된 사용자 이벤트 처리 시 유용합니다.

IMQA.userEvent.switchTo(id: string): void

매개변수:

  • id (string, required): 전환할 이벤트 ID

예시:

const mainEventId = IMQA.userEvent.start('main-workflow');
const subEventId = IMQA.userEvent.start('sub-task');

// 서브 태스크 실행
performSubTask();

// 메인 워크플로우로 다시 전환
IMQA.userEvent.switchTo(mainEventId);
performMainTask();

IMQA.userEvent.end(subEventId);
IMQA.userEvent.end(mainEventId);

userEvent.getActiveContext()

현재 활성화된 사용자 이벤트의 컨텍스트를 반환합니다.

IMQA.userEvent.getActiveContext(): Context

반환값:

  • 활성 컨텍스트 또는 ROOT_CONTEXT

예시:

const context = IMQA.userEvent.getActiveContext();
console.log('Active context:', context);

userEvent.hasActiveUserEvent()

현재 활성화된 사용자 이벤트가 있는지 확인합니다.

IMQA.userEvent.hasActiveUserEvent(): boolean

반환값:

  • 활성 이벤트 존재 여부 (boolean)

예시:

if (IMQA.userEvent.hasActiveUserEvent()) {
  console.log('User event is active');
  const activeEvent = IMQA.userEvent.getActive();
  console.log('Active event:', activeEvent);
}

userEvent.getContext()

특정 사용자 이벤트의 컨텍스트를 반환합니다.

IMQA.userEvent.getContext(id: string): Context | undefined

매개변수:

  • id (string, required): 조회할 이벤트 ID

반환값:

  • 이벤트 컨텍스트 또는 undefined

예시:

const eventId = IMQA.userEvent.start('my-event');
const context = IMQA.userEvent.getContext(eventId);

사용 예시

버튼 클릭 이벤트 추적

function handleButtonClick() {
  // 이벤트 시작
  const eventId = IMQA.userEvent.start('button-click', {
    'button.name': 'submit',
    'page.url': window.location.href,
  });

  // 비즈니스 로직 실행
  performBusinessLogic()
    .then(result => {
      // 성공 시 이벤트 완료
      IMQA.userEvent.end(eventId, {
        'action.result': 'success',
        'items.count': result.count,
      });
    })
    .catch(error => {
      // 에러 시 이벤트 완료
      IMQA.userEvent.end(eventId, {
        'action.result': 'error',
        'error.message': error.message,
      });
    });
}

폼 제출 이벤트 추적

function handleFormSubmit() {
  const eventId = IMQA.userEvent.start('form-submission', {
    'form.type': 'contact',
    'form.fields': 'name,email,message',
  });

  try {
    validateForm();
    submitForm();

    IMQA.userEvent.end(eventId, {
      'submission.result': 'success',
    });
  } catch (error) {
    IMQA.userEvent.end(eventId, {
      'submission.result': 'error',
      'error.type': error.constructor.name,
    });
    throw error;
  }
}

파일 업로드 with 취소 기능

function handleFileUpload(file) {
  const eventId = IMQA.userEvent.start('file-upload', {
    'file.name': file.name,
    'file.size': file.size,
    'file.type': file.type,
  });

  // 사용자가 취소한 경우
  cancelButton.onclick = () => {
    IMQA.userEvent.cancel(eventId);
    uploadRequest.abort();
  };

  // 정상 처리
  uploadFile(file)
    .then(response => {
      IMQA.userEvent.end(eventId, {
        'upload.result': 'success',
        'upload.url': response.url,
      });
    })
    .catch(error => {
      IMQA.userEvent.end(eventId, {
        'upload.result': 'error',
        'error.message': error.message,
      });
    });
}

다단계 워크플로우 추적

class CheckoutProcess {
  constructor() {
    this.eventId = null;
  }

  startCheckout() {
    this.eventId = IMQA.userEvent.start('checkout-workflow', {
      'cart.total': this.getCartTotal(),
      'cart.items.count': this.getCartItemCount(),
    });
  }

  processPayment() {
    if (!this.eventId) return;
    
    // 이벤트가 활성화되어 있으면 여기서 발생하는 모든 fetch, console 등이 자동으로 그룹화됨
    return paymentAPI.charge(this.paymentInfo)
      .then(result => {
        this.completeCheckout(result);
      })
      .catch(error => {
        this.failCheckout(error);
      });
  }

  completeCheckout(result) {
    if (!this.eventId) return;
    
    IMQA.userEvent.end(this.eventId, {
      'checkout.result': 'success',
      'order.id': result.orderId,
      'payment.method': result.paymentMethod,
      'total.amount': result.amount,
    });
    
    this.eventId = null;
  }

  failCheckout(error) {
    if (!this.eventId) return;
    
    IMQA.userEvent.end(this.eventId, {
      'checkout.result': 'error',
      'error.code': error.code,
      'error.message': error.message,
    });
    
    this.eventId = null;
  }

  cancelCheckout() {
    if (!this.eventId) return;
    
    IMQA.userEvent.cancel(this.eventId);
    this.eventId = null;
  }
}

중첩된 사용자 이벤트 관리

// 메인 워크플로우와 서브 태스크를 동시에 추적
function complexWorkflow() {
  const mainEventId = IMQA.userEvent.start('main-workflow', {
    'workflow.type': 'data-processing'
  });

  // 메인 워크플로우 컨텍스트에서 초기 작업 수행
  loadInitialData();

  // 서브 태스크 시작 (메인 이벤트의 자식으로 추적됨)
  const subTaskId = IMQA.userEvent.start('sub-task-validation', {
    'task.type': 'validation'
  });

  // 서브 태스크 실행
  validateData();
  IMQA.userEvent.end(subTaskId, { 'validation.result': 'passed' });

  // 메인 워크플로우로 다시 전환
  IMQA.userEvent.switchTo(mainEventId);

  // 메인 워크플로우 계속 진행
  processData();
  saveResults();

  IMQA.userEvent.end(mainEventId, { 'workflow.result': 'success' });
}

컨텍스트 기반 실행

// with 메서드를 사용한 명시적 컨텍스트 관리
async function processDataWithContext() {
  const eventId = IMQA.userEvent.start('data-processing');

  try {
    // 특정 이벤트 컨텍스트에서 비동기 작업 실행
    const result = await IMQA.userEvent.with(eventId, async () => {
      // 이 블록 내의 모든 계측이 eventId와 연결됩니다
      const rawData = await fetchRawData();
      const cleaned = await cleanData(rawData);
      const transformed = await transformData(cleaned);
      return transformed;
    });

    IMQA.userEvent.end(eventId, {
      'processing.result': 'success',
      'records.processed': result.length
    });
  } catch (error) {
    IMQA.userEvent.end(eventId, {
      'processing.result': 'error',
      'error.message': error.message
    });
  }
}

// withActive를 사용한 현재 활성 이벤트 활용
function batchOperation() {
  const eventId = IMQA.userEvent.start('batch-operation');

  const tasks = [task1, task2, task3, task4];

  // 현재 활성 이벤트 컨텍스트에서 각 작업 실행
  tasks.forEach(task => {
    IMQA.userEvent.withActive(() => {
      task.execute();
    });
  });

  IMQA.userEvent.end(eventId);
}

검색 기능 추적

function handleSearch(query) {
  const eventId = IMQA.userEvent.start('search-operation', {
    'search.query': query,
    'search.type': 'full-text',
  });

  searchAPI.search(query)
    .then(results => {
      IMQA.userEvent.end(eventId, {
        'search.result.count': results.length,
        'search.duration': results.metadata.duration,
        'search.result': 'success',
      });
    })
    .catch(error => {
      IMQA.userEvent.end(eventId, {
        'search.result': 'error',
        'error.type': error.name,
      });
    });
}

사용자 이벤트 스팬

사용자 이벤트 스팬은 커스텀 사용자 이벤트를 나타내며, 다음과 같은 내용을 포함합니다:

  • traceId: 트레이스의 고유 식별자
  • spanId: 스팬의 고유 식별자
  • parentSpanId: 부모 스팬의 식별자 (적용될 경우)
  • name: 이벤트 이름 (예: "button-click", "checkout-process")
  • kind: 스팬의 타입 (INTERNAL = 1)
  • startTimeUnixNano: 에포크 이후의 이벤트 시작 시간 (나노초)
  • endTimeUnixNano: 에포크 이후의 이벤트 완료 시간 (나노초)
  • status: 결과 상태 (OK = 0)

사용자 이벤트 스팬 속성

각 사용자 이벤트 스팬은 다음과 같은 속성을 포함합니다:

속성타입설명
event.namestring이벤트 이름
event.idstring이벤트 고유 식별자
location.hrefstring현재 페이지 URL
environmentstring환경 이름
deployment.environmentstring배포 환경
screen.namestring화면/페이지 이름
screen.typestring화면/페이지 타입
session.idstring사용자 세션 식별자
componentstring컴포넌트 이름 (@imqa/user-event)
span.typestring스팬 타입 (user-event)
커스텀 속성다양함start()end()에서 추가한 커스텀 속성

모범 사례

1. 의미 있는 이벤트 이름 사용

// 좋은 예
IMQA.userEvent.start('checkout-payment-processing');
IMQA.userEvent.start('product-search');
IMQA.userEvent.start('user-profile-update');

// 나쁜 예
IMQA.userEvent.start('event1');
IMQA.userEvent.start('click');
IMQA.userEvent.start('process');

2. 적절한 속성 추가

// 좋은 예 - 분석에 유용한 컨텍스트 제공
IMQA.userEvent.start('product-purchase', {
  'product.id': 'PROD-123',
  'product.category': 'electronics',
  'product.price': 299.99,
  'user.tier': 'premium'
});

// 나쁜 예 - 민감한 정보나 불필요한 데이터
IMQA.userEvent.start('product-purchase', {
  'credit.card.number': '1234-5678-9012-3456', // 절대 금지
  'user.password': 'secret123' // 절대 금지
});

3. 에러 처리

function trackUserAction() {
  let eventId;
  
  try {
    eventId = IMQA.userEvent.start('user-action');
    // 비즈니스 로직
    performAction();
    IMQA.userEvent.end(eventId, { result: 'success' });
  } catch (error) {
    if (eventId) {
      IMQA.userEvent.end(eventId, {
        result: 'error',
        'error.message': error.message
      });
    }
    throw error;
  }
}

4. 메모리 관리

// 이벤트 ID를 적절히 관리하고 완료되지 않은 이벤트 방지
class UserEventManager {
  constructor() {
    this.activeEvents = new Map();
  }

  start(name, attributes) {
    const id = IMQA.userEvent.start(name, attributes);
    this.activeEvents.set(id, { name, startTime: Date.now() });
    return id;
  }

  end(id, attributes) {
    if (this.activeEvents.has(id)) {
      IMQA.userEvent.end(id, attributes);
      this.activeEvents.delete(id);
    }
  }

  cleanup() {
    // 오래된 이벤트 정리 (예: 5분 이상)
    const now = Date.now();
    for (const [id, event] of this.activeEvents) {
      if (now - event.startTime > 5 * 60 * 1000) {
        IMQA.userEvent.cancel(id);
        this.activeEvents.delete(id);
      }
    }
  }
}

고급 기능

이벤트 지속성 (Persistence)

UserEvent는 페이지 새로고침 시에도 이벤트 상태를 유지할 수 있도록 localStorage에 이벤트 정보를 저장합니다. 이를 통해 다음과 같은 시나리오를 처리할 수 있습니다:

  • 페이지 새로고침 중에도 진행 중인 이벤트 추적
  • 멀티 페이지 워크플로우에서 일관된 이벤트 추적
  • 예기치 않은 페이지 이탈 후 이벤트 복원
// 이벤트 시작 (localStorage에 자동 저장됨)
const eventId = IMQA.userEvent.start('long-running-task');

// 페이지 새로고침 발생...

// 페이지 로드 후 자동으로 이벤트 복원
// IMQA 초기화 시 저장된 이벤트들이 자동으로 복원됩니다

컨텍스트 전파 (Context Propagation)

사용자 이벤트는 OpenTelemetry의 컨텍스트 전파 메커니즘을 활용하여 이벤트 실행 중 발생하는 모든 계측을 자동으로 그룹화합니다:

const eventId = IMQA.userEvent.start('api-workflow');

// 이 시점부터 발생하는 모든 계측이 eventId와 연결됩니다:
// - fetch 요청
// - XMLHttpRequest
// - console 로그
// - 예외 발생
// - 사용자 상호작용
// - 커스텀 로그

await fetch('/api/data');  // eventId 컨텍스트로 추적됨
console.log('Processing'); // eventId 컨텍스트로 추적됨

IMQA.userEvent.end(eventId);

중첩 이벤트 관리

여러 사용자 이벤트를 중첩하여 복잡한 워크플로우를 세밀하게 추적할 수 있습니다:

const parentEventId = IMQA.userEvent.start('parent-operation');

// 자식 이벤트 (parent의 컨텍스트 하위에 생성됨)
const childEventId = IMQA.userEvent.start('child-task');
performChildTask();
IMQA.userEvent.end(childEventId);

// 부모 이벤트로 자동 전환되어 계속 추적
performParentTask();
IMQA.userEvent.end(parentEventId);

주의사항

초기화 요구사항

  • 사용자 이벤트는 IMQA가 초기화된 후에만 사용할 수 있습니다
  • Tracer가 설정되기 전에 호출하면 에러가 발생합니다

이벤트 생명주기

  • 이벤트 ID는 고유하며, 한 번만 종료하거나 취소할 수 있습니다
  • 종료되지 않은 이벤트는 메모리와 localStorage에 계속 유지됩니다
  • 장시간 실행되는 이벤트는 적절히 관리하여 메모리 누수를 방지하세요

보안 및 개인정보

  • 민감한 개인 정보를 속성으로 추가하지 마세요
  • 신용카드 번호, 비밀번호, 개인식별정보 등은 절대 포함하지 마세요

성능 고려사항

  • cancel()을 호출하면 span이 기록되지 않으므로, 실제로 취소된 경우에만 사용하세요
  • 너무 많은 중첩 이벤트는 추적 데이터를 복잡하게 만들 수 있습니다
  • 이벤트 이름과 속성은 의미있고 일관성 있게 작성하세요

localStorage 제한

  • 브라우저의 localStorage 용량 제한(일반적으로 5-10MB)을 고려하세요
  • 프라이빗 브라우징 모드에서는 localStorage가 제한적으로 동작할 수 있습니다

디버깅

콘솔에 UserEvent 관련 로그가 출력되어 이벤트 생명주기를 추적할 수 있습니다:

// 이벤트 시작 시
// [UserEvent] Started event abc123 with context. All instrumentations will now use this context.

// 이벤트 종료 시
// [UserEvent] Ended event abc123, no more active events. Context reset to ROOT.

// 컨텍스트 전환 시
// [UserEvent] Switched context to event xyz789

// 이벤트 복원 시
// Restored current active event ID: abc123

이러한 로그를 통해 이벤트의 시작, 종료, 전환 시점을 파악하고 컨텍스트 관리가 올바르게 이루어지는지 확인할 수 있습니다.. 또한 취소한 경우에 이벤트 구간에 계측된 다른 계측 span들은 기록이 됩니다. 이때 rootless span이 생성될 수 있음을 유의하세요