Never give up

Node - Google analytics (feat. Chart.js, Excel) 본문

WEB

Node - Google analytics (feat. Chart.js, Excel)

대기만성 개발자 2022. 4. 22. 17:02
반응형

이번에는 google analytics에서 축적된(?) 데이터를

 

chart js로 시각화 그리고 excel파일로 만들어서 다운받는 예제를 만들어봤습니다

 

먼저 google cloud platform에서 api활성화, 그리고 계정 생성, domain 설정(필자는 localhost:8080), 환경변수 등록이 필요합니다

 

자세한 내용은 링크를 참고해주세요

(링크: https://developers.google.com/analytics/devguides/reporting/data/v1/quickstart-client-libraries#node.js_1)

 

위 링크대로 api를 활성화 하고, credentials.json파일을 적당한곳(필자는 프로젝트 폴더)에 넣어줍니다

 

그 후 환경변수에 다음과 같이 등록해줍니다

(윈도우는 링크 참고 : https://cloud.google.com/docs/authentication/production)

< 맥 기준 >

server.js

require('dotenv').config()

const express = require('express')

const app = express()

const port = process.env.PORT || 8080

const show = require('./show')
const report = require('./report')

// http://localhost:8080/show/analytics.html <- show chart page
app.use('/show', show)

// http://localhost:8080/report/0 <- get report by date
// http://localhost:8080/report/1 <- get report by screen
// http://localhost:8080/report/file/excel <- make and download excel file
app.use('/report', report)

app.listen(port, () => {
    console.log(`start! express server on port ${port}`)
})

일단 기본적인 셋팅 + route를 조금 셋팅 해줬는데

 

간단하게 정리해보자면

 

 - 날짜별 report 가져오기 : http://localhost:8080/report/0

 - 화면별 report 가져오기 : http://localhost:8080/report/1

 

 - 차트 보여주기 : http://localhost:8080/show/analytics.html

 - 엑셀파일 다운로드 : http://localhost:8080/report/file/excel

 

정도로 만들어봤습니다

 

report.js를 먼저 보자면

const express = require("express")

const router = express.Router()

const excel = require('./excel')

const { BetaAnalyticsDataClient } = require('@google-analytics/data');

async function runReport(name) {
    const propertyId = process.env.PROPERTY_ID;

    const analyticsDataClient = new BetaAnalyticsDataClient();

    let reportData = []

    const [response] = await analyticsDataClient.runReport({
        property: `properties/${propertyId}`,
        dateRanges: [
            {
                startDate: '7daysAgo',
                endDate: 'yesterday',
            },
        ],
        dimensions: [
            {
                name: name,
            }
        ],
        metrics: [
            {
                name: 'activeUsers'
            },
            {
                name: 'active1DayUsers'
            },
            {
                name: 'active7DayUsers'
            },
            {
                name: 'averageSessionDuration'
            },
            {
                name: 'dauPerWau'
            },
            {
                name: 'eventCountPerUser'
            },
            {
                name: 'screenPageViewsPerSession'
            },
            {
                name: 'totalUsers'
            }
        ],
    });

    console.log('Report result:', response.rows.length);
    response.rows.forEach((row, rowIndex) => {
        // console.log({ rowIndex, row })
        let data = { div: row.dimensionValues[0].value, list: [] }

        row.metricValues.forEach((e, i) => {
            switch (i) {
                case 0:
                    data.list = [{ title: '앱 사용자 수', value: e.value }]
                    break
                case 1:
                    data.list = [...data.list, { title: '1일 사용자 수', value: e.value }]
                    break
                case 2:
                    data.list = [...data.list, { title: '1주 사용자 수', value: e.value }]
                    break
                case 3:
                    data.list = [...data.list, { title: '앱 평균 사용시간(세션 유지 시간)', value: Math.round(e.value) }]
                    break
                case 4:
                    data.list = [...data.list, { title: '1주 순 이용자수(dau)', value: Math.round(e.value) }]
                    break
                case 5:
                    data.list = [...data.list, { title: '1인 당 이벤트(클릭 등) 횟수', value: Math.round(e.value) }]
                    break
                case 6:
                    data.list = [...data.list, { title: '1인 당 페이지 뷰 수', value: Math.round(e.value) }]
                    break
                case 7:
                    data.list = [...data.list, { title: '전체 유저 수', value: e.value }]
                    break
                default:
                    console.log('??', e.value)
            }
        })

        reportData = [...reportData, data]
    });
    reportData.sort((a, b) => a.div - b.div)
    // report data = [{date: 날짜, list: [{title: metric, value: value}]}]
    // console.log(JSON.stringify(reportData))

    return reportData
}

// report 데이터 가져오기
// 0 : date, 1 : unifiedScreenName
router.get('/:no', async (req, res) => {
    const no = req.params.no

    let data
    let code

    try {
        data = await runReport(no == 0 ? 'date' : 'unifiedScreenName')
        code = 1
        console.log({ data })
    } catch (e) {
        data = e
        code = -1
        console.log({ e })
    }

    res.send({ data, code })
})

// 현재 디렉토리에 analytics.xls 추가
router.get('/file/excel', async (req, res) => {
    let data
    let code

    try {
        data = {}
        data.date = await runReport('date')
        data.screen = await runReport('unifiedScreenName')
        data.finished = () => {
            console.log('Downloading is started')
            res.download('./analytics.xls')
            console.log('Downloading is finished')
        }
        excel.makeExcel(data)

        code = 1
    } catch (e) {
        data = e
        code = -1

        res.send({ data, code })
    }
})

module.exports = router

필자는 구글 report api콜을 위해 npm에서 @google-analytics/data를 이용했습니다

(링크 : https://www.npmjs.com/package/@google-analytics/data)

 

run report()함수에서 name에 따라 dimension값을 설정해주고,

 

각각 파라미터(metrics)로

  1. 앱 사용자 수
  2. 1일 사용자 수
  3. 1주 사용자 수
  4. 앱 평균 사용시간(세션 유지 시간)
  5. 1주 순 이용자수
  6. 1인 당 사용 이벤트 발생 수(클릭 등)
  7. 1인 당 페이지 뷰 수
  8. 전체 유저 수

를 받고 있습니다(참고로 필자는 한 주정도의 데이터를 확인중입니다)

 

그리고 제가 사용하기 좋은 형태의 데이터로 만들기 위해 switch문으로 적당히(?) 저장해줍니다

 

최종 데이터 타입은 [{div: 날짜, list: [{tittle: 항목, value: 트래킹된 값]}]…}로

 

Array와 Object가 중첩된 형태입니다 말로 풀어쓰자면..

 

Array안에 Object(날짜, 항목별 Array) 항목별 Array 내부에 Object는 항목 그리고 값이 들어갑니다

(플러터로 치자면.. List<Map<String, List<Map<String, dynamic>>>> )

 

그리고 router부분을 보면

 

1. /:no : no값에 따라서 date혹은 screen name을 보여줍니다

2. /file/excel : excel파일을 만들고 download를 해줍니다

 

excel로 만드는부분을 보면

 

excel.js

const reader = require('xlsx')

exports.makeExcel = (data) => {
    const workBook = handler.getWorkBook()
    // 날짜별 통계값 엑셀 형태로
    const dateSheet = handler.getWorksheet(data.date)
    // 페이지별 통계값 엑셀 형태로
    const screenSheet = handler.getWorksheet(data.screen)

    // sheet에 추가
    reader.utils.book_append_sheet(workBook, dateSheet, handler.getSheetName('날짜'))
    reader.utils.book_append_sheet(workBook, screenSheet, handler.getSheetName('페이지'))

    // excel 파일 생성
    // reader.writeFile(workBook, handler.getExcelFileName())
    reader.writeFileAsync(handler.getExcelFileName(), workBook, null, () => {
        console.log('Finished writing')
        data.finished()
    })
}

const handler = {
    getWorkBook: () => {
        return reader.utils.book_new()
    },
    getExcelFileName: () => {
        return `analytics.xls`;
    },
    getSheetName: (name) => {
        // 공백이면 순서대로
        // ex: sheet1, sheet2
        return name;
    },
    getExcelData: (data) => {

        let sheetData = [['']]

        data.forEach((row, rowIndex) => {
            console.log({ row, rowIndex })
            sheetData[0] = [...sheetData[0], row.div]

            row.list.forEach((e, i) => {
                console.log({ e, i })
                if (rowIndex === 0) {
                    sheetData[i + 1] = [e.title, e.value]
                } else {
                    sheetData[i + 1] = [...sheetData[i + 1], e.value]
                }
            })
        })

        return sheetData;
    },
    getWorksheet: (data) => {
        console.log('getWorksheet : ', data.length)
        return reader.utils.aoa_to_sheet(handler.getExcelData(data));
    }
}

엑셀 관련 라이브러리를 찾던중 xlsx 라이브러리 이용자수가 가장 많아서 사용해봤습니다

(링크 : https://www.npmjs.com/package/xlsx)

 

위에서 만들어놓은 api로 가져온 데이터를 이용해서 보기 좋은 형태로 만들어서

(handler의 getExcelData 부분 참고)

 

book_append_sheet 함수를 이용해서 sheet에 추가해주고 writeFileAsync함수로 엑셀파일을 만들어줍니다

(그냥 writeFile은 생성완료 콜백이 따로 없어서 해당 함수를 사용했습니다)

 

그리고 생성이 완료되면 콜백을 이용해서 res.download(엑셀파일 경로)로 클라이언트 pc에 저장을 해줍니다

 

< 다운로드 완료! >
< 다운받은 excel파일 >

(데이터는 민감한(?)정보여서 일단 가려놨습니다)

 

show.js

const express = require("express")

const router = express.Router()

const fs = require('fs')

router.get('/analytics.html', (_,res) => {
    fs.readFile('./analytics.html', (err, data) => {
        if(err){
            throw err
        }

        res.end(data)
    })
})

module.exports = router

폴더 내에 analytics.html를 찾아서 보여주는 역할을 합니다

 

analytics.html

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <title>G9bon analytics</title>
    <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.7.1/chart.min.js"></script>
</head>

<body>
    <script>
        // 0: date, 1: screen
        fetch('/report/0')
            .then((res) => res.json())
            .then((res) => {
                if (res) {
                    console.log('done')
                    makeChart(res.data,'line')
                }
            })
            .catch((e) => {
                alert(e)
            })

        setTimeout(() => {
            fetch('/report/1')
                .then((res) => res.json())
                .then((res) => {
                    if (res) {
                        console.log('done')
                        makeChart(res.data, 'bar')
                    }
                })
                .catch((e) => {
                    alert(e)
                })
        }, 3000);


        const makeChart = (report, type) => {
            let labels = []
            let datasets = Array.from({ length: report[0].list.length }, (_) => {
                return {
                    label: '', backgroundColor: type != 'line' ? [
                        'rgba(255, 99, 132, 0.8)',
                        'rgba(255, 159, 64, 0.8)',
                        'rgba(255, 205, 86, 0.8)',
                        'rgba(75, 192, 192, 0.8)',
                        'rgba(54, 162, 235, 0.8)',
                        'rgba(153, 102, 255, 0.8)',
                        'rgba(201, 203, 207, 0.8)',
                        'rgba(255, 99, 132, 0.5)',
                        'rgba(255, 159, 64, 0.5)',
                        'rgba(255, 205, 86, 0.5)',
                        'rgba(75, 192, 192, 0.5)',
                        'rgba(54, 162, 235, 0.5)',
                        'rgba(153, 102, 255, 0.5)',
                        'rgba(201, 203, 207, 0.5)',
                        'rgba(255, 99, 132, 0.2)',
                        'rgba(255, 159, 64, 0.2)',
                        'rgba(255, 205, 86, 0.2)',
                        'rgba(75, 192, 192, 0.2)',
                        'rgba(54, 162, 235, 0.2)',
                        'rgba(153, 102, 255, 0.2)',
                        'rgba(201, 203, 207, 0.2)'
                    ] : 'rgb(255, 99, 132)', borderColor: type != 'line' ? 'rgb(255,255,255)' : 'rgb(255, 99, 132)', data: []
                }
            })

            report.forEach((reportList, rIndex) => {
                labels = [...labels, reportList.div]

                reportList.list.forEach((e, i) => {

                    console.log(i, e.title, datasets[i]?.data ?? [], datasets.length)

                    datasets[i].label = e.title
                    datasets[i].data = [...datasets[i]?.data ?? [], e.value]
                })

                console.log({ rIndex })
            })

            datasets.forEach((e, i) => {
                const data = {
                    labels,
                    datasets: [e]
                }

                const config = {
                    type: type,
                    data: data,
                    options: {}
                }

                const div = document.createElement('div')

                document.body.appendChild(div)

                const canvas = document.createElement('canvas')

                div.appendChild(canvas)

                const myChart = new Chart(canvas, config)
            })

        }
    </script>
</body>

</html>

cdn으로 chart js를 설치해주고, fetch를 통해 api콜을 한 이후에

 

결과값을 이용해서 makeChart()함수를 이용해 차트를 그려주는데

 

위에서 만든 데이터를 가지고 canvas에 chart js형식에 맞춰서 적당히(?) 넣어주고

 

append child로 하나씩 더해주는 형태로 만들어봤습니다

< Chart js로 그린 그래프 >

(마찬가지로 데이터는 민감한(?) 정보라 왼쪽은 최대한 가렸습니다)

 

하단에 더 많은 그래프가 있는데, 일단 잘 나온다는것만 보여주기용으로 하나만 첨부해봤습니다

 

(전체 소스 코드 링크 : https://github.com/devmemory/analytics)

 

작업하면서 데이터 타입이 지저분하다보니(?) 사용하기 좋은 형태로 가공하는데 시간을 조금 많이 쓴것 같습니다

(필자가 머리가 안좋아서..)

반응형

'WEB' 카테고리의 다른 글

Node - XLSX example (feat. scraping)  (0) 2022.05.11
Node - Nodemailer example  (2) 2022.04.26
React, Node - Image upload example  (0) 2022.02.19
React - Kakaomap example  (0) 2022.02.19
React - html, css album page clone  (0) 2022.01.08
Comments