DevOps

Datadog Uploading JavaScript Source Maps

mechaniccoder 2022. 12. 14. 02:22

최근 production 환경에서 발생하는 에러가 어느 코드 부분에서 발생했는지 자세히 알기 위해 datadog uploading source map 설정을 했습니다. 이 과정에 대해 공유해보려 합니다.


이 포스팅을 읽고 난 뒤에는 아래의 내용들을 이해하게 됩니다.

  • Next.js에서 source map 설정하기
  • Production 환경에서 안전하게 source map 다루기
  • Datadog source map 업로드하기
  • GitHub Actions를 사용해 Vercel build, deploy하기

What is source map

먼저 source map이 뭔지부터 알아야 합니다. 간략하게 설명하자면 빌드된 파일과 원본 파일을 매핑시켜주는 역할을 하는 파일이라 생각하시면 됩니다. 자세한 설명은 링크로 대체하겠습니다.

Source map setting in Next.js

source map에 대해서 알았다면 Next.js에서 source map을 생성하기 위한 설정은 다음과 같습니다.

// next.config.js
module.exports = {
  productionBrowserSourceMaps: true,
}

실무에서 설정을 할 때는 production 환경에서만 source map 파일을 생성하기 위해 다음과 같이 environment variable을 활용했습니다.

 // next.config.js
 const enableSourceMap = process.env.ENABLE_SOURCE_MAP

 module.exports = {
     productionBrowserSourceMaps: enableSourceMap
 }

제가 설정했던 프로젝트는 TypeScript로 되어있었기 때문에 추가적인 설정이 필요했습니다.

// tsconfig.json
{
  "sourceMap": true,
  //...
}

위의 설정을 마친 뒤 build를 하면 *.js.map 형태의 source map 파일이 생성되는 것을 확인할 수 있습니다.

Build source map in github actions

매번 local에서 source map을 생성할 수는 없으니 코드가 github의 production branch에 push됐을때 workflow를 수행하도록 해보겠습니다.


github actions의 worflow는 아래와 같습니다.

name: Deploy Production

on:
  push:
    branches: [main]

jobs:
  deploy-production:
    runs-on: ubuntu-latest
    environment: production

    steps:
      - uses: actions/checkout@v3

      - name: Setup Node.js environment
        uses: actions/setup-node@v3
        with:
          node-version: 16
          cache: yarn

      - name: Cache dependencies
        uses: actions/cache@v3
        id: yarn-cache
        with:
          path: '**/node_modules'
          key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}

      - name: Install dependencies
        if: steps.yarn-cache.outputs.cache-hit != 'true'
        run: yarn install --frozen-lockfile

      - name: Cache build output
        uses: actions/cache@v3
        id: build-cache
        with:
          path: '**/.vercel/output'
          key: ${{ runner.os }}-yarn-${{ hashFiles('**/.vercel/output') }}

      - name: Build app
        if: steps.build-cache.outputs.cache-hit != 'true'
        run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
        env:
          ENABLE_SOURCE_MAP: ${{ secrets.ENABLE_SOURCE_MAP }}

여기서 눈 여겨 볼 수 있는건 마지막에 vercel build를 활용한 것입니다. vercel은 github와 자동으로 integration이 되기 때문에 code가 branch에 push가 될 경우 빌드, 배포가 자동으로 수행됩니다. 그럼 vercel에서 하도록 놔두면 되지 왜 github action에서 빌드를 해야할까요? 다음 섹션에서 자세히 알아보도록 하겠습니다.

Vercel build API

source map이 올바르게 작동하려면 source map이 바라보고 있는 파일이 배포가 되어야 합니다. 아래 예시를 들어보죠.


github action에서 빌드를 하게되면 아래와 같이 파일들을 얻게 됩니다. - 125-aijfiejawek.js.map - 125-aijfiejawek.js - ...
vercel에서도 빌드를 자동으로 수행하니 마찬가지로 빌드된 파일을 얻게 되겠죠? 다만, vercel에서는 source map 관련 cli를 설정하지 않았으니 source map를 얻지는 못할 겁니다.

  • 39-difjeidkfjae.js
  • ...


여기서 문제는 vercel에서는 `39-difjeidkfjae.js`가 포함된 디렉토리 경로의 파일들을 배포 파일로 사용한다는 점 입니다. github actions에서 source map이 바라보고 있는 파일이 아니기 때문에 의미가 없는 것이죠. 따라서 github actions에서 빌드된 파일을 vercel에서 배포하도록 설정을 해줘야 합니다. 바로 이와 같은 케이스를 위해 [vercel의 build output api](https://vercel.com/guides/how-can-i-use-github-actions-with-vercel)를 사용했습니다.

Datadog source map uploading

source map과 build output을 얻었으니 datadog에 source map을 업로드해야 합니다.

        # ...

      - name: Pull Vercel Environment Information
        if: steps.build-cache.outputs.cache-hit != 'true'
        run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}

      - name: Build app
        if: steps.build-cache.outputs.cache-hit != 'true'
        run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
        env:
          ENABLE_SOURCE_MAP: ${{ secrets.ENABLE_SOURCE_MAP }}

      - name: Upload source map to datadog
        run: |
          npx datadog-ci sourcemaps upload ${{ env.VERCEL_OUTPUT_PATH }} \
          --service=${{ env.YOUR_SERVICE_NAME }} \
          --release-version=${{ env.YOUR_RELEASE_VERSION }} \
          --minified-path-prefix=${{ env.YOUR_MINIFIED_PATH_PREFIX }}

datadog-ci의 옵션중에 minified-path-prefix 옵션은 실수할 수 있는 부분이라 좀 더 살펴보겠습니다.

이 옵션은 배포된 파일들을 production 도메인에서 어떤 경로로 찾을 수 있는지를 설정하는 부분입니다. 예를 들어보죠.
production 도메인은 example.com이고 minified된 파일들은 next.js의 경로에 맞춰 _next/static/chunks/에 있다고 했을 경우 다음과 같이 옵션을 설정할 수 있습니다.

      - name: Upload source map to datadog
        run: |
          npx datadog-ci sourcemaps upload ./.vercel/output/static \
          --minified-path-prefix=https://example.com
          # ...other options

github actions에서 빌드된 파일의 경로는 .vercel/output/static/_next/static/chunks/192-4aecd962efd9ec8ad.js.map 이므로 위와 같이 build path를 ./vercel/output/static으로 설정해주면 해당 경로 하위의 디렉토리 경로가 minified-path-prefix 뒤에 붙게됩니다. 최종적으로 https://example.com/_next/static/chunks/192-4aecd962efd9ec8ad.js 에서 error가 발생하면 이를 바라보는 source map인 192-4aecd962efd9ec8ad.js.map를 찾아보게 되는 것이죠.

Delete source map files

빌드 및 source map 업로드를 완료했으니 이제는 배포를 해야합니다. vercel의 deploy cli를 활용하면 됩니다. 주의해야할 점으로는 생성한 source map 파일들을 꼭 삭제해야 합니다. vercel deploy가 vercel의 build output 디렉토리를 배포하게 되면 source map까지 같이 올라가기 때문에 production에서 모든 코드들이 노출되게 됩니다.


아 참고로 source map을 삭제하기 위해 [`rimraf`](https://www.npmjs.com/package/rimraf) 패키지를 사용했습니다.

    # ...other steps

      - name: Upload source map to datadog
        run: |
          npx datadog-ci sourcemaps upload ./.vercel/output/static \
          --service=${{ env.YOUR_SERVICE_NAME }} \
          --release-version=${{ env.YOUR_RELEASE_VERSION }} \
          --minified-path-prefix=https://example.com

        # 주의하세요!
      - name: Delete sourcemap
        run: yarn delete:sourcemap # rimraf .vercel/**/*.map

      - name: Deploy Project Artifacts to Vercel
        run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}

마치며

이번에 설정한 datadog source map 시스템이 잘 활용돼서 팀원들의 production 디버깅을 재밌고 원할하게 하며 유저들의 report보다 더 빠르게 해결할 수 있도록 도움이 되었으면 좋겠네요.


저도 배운점들이 많았던 것 같습니다. 특히 source map과 관련 설정들에 대해 더 잘 이해하게 됐고 github actions과 vercel 각각 따로 빌드되는 문제를 어떻게 해결할 수 있을까에 대한 고민도 의미있었던 것 같습니다.
개인적으로 생각해봤을때 지금보다 개선할 수 있는 것들은 다음과 같습니다.

  • vercel에서 자동으로 build를 돌리지 않도록 ignore하기

References