careerly-data-mcp 2.0.1 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +183 -137
- package/dist/bigquery/client.d.ts +8 -1
- package/dist/bigquery/client.d.ts.map +1 -1
- package/dist/bigquery/client.js +118 -52
- package/dist/bigquery/client.js.map +1 -1
- package/dist/bigquery/types.d.ts +6 -0
- package/dist/bigquery/types.d.ts.map +1 -1
- package/dist/bigquery/types.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +12 -2
- package/dist/mcp/server.js.map +1 -1
- package/dist/tools/bq-event-params.d.ts +10 -0
- package/dist/tools/bq-event-params.d.ts.map +1 -0
- package/dist/tools/bq-event-params.js +266 -0
- package/dist/tools/bq-event-params.js.map +1 -0
- package/dist/tools/bq-funnel.d.ts +10 -0
- package/dist/tools/bq-funnel.d.ts.map +1 -0
- package/dist/tools/bq-funnel.js +358 -0
- package/dist/tools/bq-funnel.js.map +1 -0
- package/dist/tools/bq-query.d.ts.map +1 -1
- package/dist/tools/bq-query.js +6 -1
- package/dist/tools/bq-query.js.map +1 -1
- package/dist/tools/bq-raw-sql.d.ts +10 -0
- package/dist/tools/bq-raw-sql.d.ts.map +1 -0
- package/dist/tools/bq-raw-sql.js +188 -0
- package/dist/tools/bq-raw-sql.js.map +1 -0
- package/dist/tools/bq-user-journey.d.ts +10 -0
- package/dist/tools/bq-user-journey.d.ts.map +1 -0
- package/dist/tools/bq-user-journey.js +289 -0
- package/dist/tools/bq-user-journey.js.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,33 @@
|
|
|
1
|
-
# Careerly
|
|
1
|
+
# Careerly Data MCP Server
|
|
2
2
|
|
|
3
|
-
Claude Code에서
|
|
3
|
+
Claude Code에서 **자연어로 GA4와 BigQuery 데이터를 분석**하는 MCP(Model Context Protocol) 서버입니다.
|
|
4
|
+
|
|
5
|
+
## 왜 MCP를 쓰나요? (vs API 직접 호출)
|
|
6
|
+
|
|
7
|
+
### 기존 방식의 문제점
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
GA4 데이터 보고 싶다 → GA4 API 문서 읽기 → 인증 설정 → 코드 작성 → 결과 해석
|
|
11
|
+
BigQuery 분석하고 싶다 → SQL 작성 → UNNEST 문법 검색 → 쿼리 수정 → 결과 해석
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
### MCP 방식
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
"지난 7일 세션수 보여줘" → 끝!
|
|
18
|
+
"page_view → post_detail_view 전환율 분석해줘" → 끝!
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### 핵심 장점
|
|
22
|
+
|
|
23
|
+
| 기존 API 직접 호출 | MCP 사용 |
|
|
24
|
+
|------------------|---------|
|
|
25
|
+
| API 문서 읽고 코드 작성 필요 | 자연어로 질문하면 끝 |
|
|
26
|
+
| GA4 event_params 추출 = 복잡한 UNNEST SQL | `bq_event_params` 도구로 한 줄 |
|
|
27
|
+
| 퍼널 분석 = 10줄 이상 SQL | `bq_funnel` 도구로 자동 계산 |
|
|
28
|
+
| 결과는 숫자만 | **인사이트 + 다음 액션 제안**까지 |
|
|
29
|
+
|
|
30
|
+
---
|
|
4
31
|
|
|
5
32
|
## 빠른 시작
|
|
6
33
|
|
|
@@ -8,110 +35,189 @@ Claude Code에서 자연어로 GA4 데이터를 분석하는 MCP(Model Context P
|
|
|
8
35
|
# 1. Service Account JSON 키 파일이 있는 폴더에서 실행
|
|
9
36
|
cd /path/to/your/project
|
|
10
37
|
|
|
11
|
-
# 2. 설정 실행 (키 파일 자동 탐지 + Claude Code 자동
|
|
12
|
-
npx careerly-
|
|
38
|
+
# 2. 설정 실행 (키 파일 자동 탐지 + Claude Code 자동 등록)
|
|
39
|
+
npx careerly-data-mcp setup
|
|
13
40
|
|
|
14
41
|
# 3. Claude Code 재시작 후 /mcp 확인!
|
|
15
42
|
```
|
|
16
43
|
|
|
17
44
|
### 자동 설정 기능
|
|
18
45
|
|
|
19
|
-
`setup`
|
|
20
|
-
|
|
21
|
-
1. **Service Account 키 파일 자동 탐지** - 현재 폴더에서 JSON 파일 스캔
|
|
22
|
-
2. **GA4 연결 테스트** - Property ID와 키 파일 검증
|
|
23
|
-
3. **Claude Code MCP 자동 등록** - `claude mcp add` 명령어로 자동 설정
|
|
46
|
+
`setup` 명령어가 알아서 처리합니다:
|
|
24
47
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
48
|
+
1. **Service Account 키 파일 자동 탐지** - 폴더에서 JSON 파일 스캔
|
|
49
|
+
2. **GA4 + BigQuery 연결 테스트** - Property ID와 키 파일 검증
|
|
50
|
+
3. **Claude Code MCP 자동 등록** - 재시작만 하면 바로 사용 가능
|
|
28
51
|
|
|
29
|
-
|
|
30
|
-
✔ 1개의 Service Account 키 파일 발견
|
|
31
|
-
✔ 사용할 키 파일 선택 my-project-key.json (my-project-id)
|
|
32
|
-
✔ 연결 성공!
|
|
33
|
-
✔ Claude Code MCP 서버 등록 완료
|
|
34
|
-
|
|
35
|
-
✓ 설정 완료!
|
|
36
|
-
```
|
|
52
|
+
---
|
|
37
53
|
|
|
38
|
-
## 사전 준비
|
|
54
|
+
## 사전 준비 (관리자)
|
|
39
55
|
|
|
40
56
|
### 1. Google Cloud 설정
|
|
41
57
|
|
|
42
58
|
1. [Google Cloud Console](https://console.cloud.google.com/)에서 프로젝트 생성
|
|
43
59
|
2. **Google Analytics Data API** 활성화
|
|
44
|
-
3.
|
|
60
|
+
3. **BigQuery API** 활성화
|
|
61
|
+
4. **서비스 계정** 생성 후 JSON 키 다운로드
|
|
45
62
|
|
|
46
63
|
### 2. GA4 권한 설정
|
|
47
64
|
|
|
48
|
-
|
|
49
|
-
|
|
65
|
+
- GA4 Admin > 속성 설정 > 속성 액세스 관리
|
|
66
|
+
- 서비스 계정 이메일 추가 (뷰어 권한)
|
|
67
|
+
|
|
68
|
+
### 3. BigQuery 권한 설정
|
|
69
|
+
|
|
70
|
+
- IAM & Admin > 서비스 계정에 **BigQuery 데이터 뷰어** 역할 추가
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## 제공 도구 (Tools)
|
|
75
|
+
|
|
76
|
+
### GA4 Data API 도구
|
|
50
77
|
|
|
51
|
-
|
|
78
|
+
| Tool | 설명 | 언제 쓰나요? |
|
|
79
|
+
|------|------|------------|
|
|
80
|
+
| `ga4_query` | GA4 데이터 조회 + 인사이트 | 세션, 사용자, 전환 등 기본 지표 |
|
|
81
|
+
| `ga4_metadata` | 지표/차원 목록 | 어떤 데이터를 볼 수 있는지 확인 |
|
|
82
|
+
| `ga4_status` | 연결 상태 확인 | 연결 문제 진단 |
|
|
52
83
|
|
|
53
|
-
|
|
84
|
+
### BigQuery 기본 도구
|
|
54
85
|
|
|
55
|
-
| Tool | 설명 |
|
|
56
|
-
|
|
57
|
-
| `
|
|
58
|
-
| `
|
|
59
|
-
| `
|
|
86
|
+
| Tool | 설명 | 언제 쓰나요? |
|
|
87
|
+
|------|------|------------|
|
|
88
|
+
| `bq_query` | BigQuery 테이블 조회 | 일반 테이블 데이터 조회 |
|
|
89
|
+
| `bq_metadata` | 스키마/테이블 목록 | 데이터 구조 파악 |
|
|
90
|
+
| `bq_status` | 연결 상태 확인 | 연결 문제 진단 |
|
|
91
|
+
| `bq_ga4_events` | GA4 Export 이벤트 조회 | event_params 자동 추출 |
|
|
92
|
+
|
|
93
|
+
### BigQuery 고급 도구 (v2.1.0 신규)
|
|
94
|
+
|
|
95
|
+
| Tool | 설명 | 언제 쓰나요? |
|
|
96
|
+
|------|------|------------|
|
|
97
|
+
| `bq_raw_sql` | SQL 직접 실행 | 복잡한 JOIN, 윈도우 함수 등 |
|
|
98
|
+
| `bq_event_params` | 이벤트 파라미터 추출 | page_location, page_title 등 쉽게 추출 |
|
|
99
|
+
| `bq_user_journey` | 사용자 여정 분석 | 특정 사용자가 어떤 행동을 했는지 |
|
|
100
|
+
| `bq_funnel` | 퍼널 분석 | 단계별 전환율 자동 계산 |
|
|
101
|
+
|
|
102
|
+
---
|
|
60
103
|
|
|
61
104
|
## 사용 예시
|
|
62
105
|
|
|
63
|
-
Claude Code에서 자연어로
|
|
106
|
+
Claude Code에서 자연어로 질문하세요:
|
|
107
|
+
|
|
108
|
+
### 기본 질의
|
|
64
109
|
|
|
65
110
|
```
|
|
66
111
|
"지난 7일 세션수 보여줘"
|
|
67
112
|
"채널별 전환수 알려줘"
|
|
68
113
|
"어제 대비 오늘 트래픽 어때?"
|
|
69
114
|
"랜딩 페이지별 이탈률 분석해줘"
|
|
70
|
-
"캠페인별 성과 비교해줘"
|
|
71
115
|
```
|
|
72
116
|
|
|
117
|
+
### 고급 질의 (v2.1.0)
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
"page_view 이벤트에서 page_location 파라미터 추출해줘"
|
|
121
|
+
"사용자 ID 12345678의 행동 여정 보여줘"
|
|
122
|
+
"page_view → post_detail_view → post_create_start 퍼널 분석해줘"
|
|
123
|
+
"디바이스별 전환율 비교해줘"
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### 실제 결과 예시 (퍼널 분석)
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
## 요약
|
|
130
|
+
page_view → post_create_start 전환율: 2.3% | 1,215 → 28명
|
|
131
|
+
|
|
132
|
+
## 인사이트
|
|
133
|
+
1. 전체 전환율: 2.3% (1,215명 중 28명 완료)
|
|
134
|
+
2. 최대 이탈 구간: page_view → post_detail_view (89.8% 이탈)
|
|
135
|
+
3. post_detail_view → post_create_start: 22.8% 전환 (개선 필요)
|
|
136
|
+
|
|
137
|
+
## 퍼널 시각화
|
|
138
|
+
1. page_view ██████████████████████████████ 1,215명 (100%)
|
|
139
|
+
2. post_detail_view ███░░░░░░░░░░░░░░░░░░░░░░░░░░░ 123명 (10.1%)
|
|
140
|
+
3. post_create_start █░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 28명 (2.3%)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## v2.1.0 주요 업데이트
|
|
146
|
+
|
|
147
|
+
### 버그 수정
|
|
148
|
+
|
|
149
|
+
- `bq_ga4_events` GROUP BY 오류 해결
|
|
150
|
+
- 이제 metrics 있을 때/없을 때 모두 정상 동작
|
|
151
|
+
|
|
152
|
+
### 새 기능
|
|
153
|
+
|
|
154
|
+
- **복합 필터 지원**: `filterLogic: "AND" | "OR"` 옵션 추가
|
|
155
|
+
- **Raw SQL 실행**: 복잡한 쿼리도 직접 작성 가능 (SELECT만 허용)
|
|
156
|
+
|
|
157
|
+
### 새 도구 4개 추가
|
|
158
|
+
|
|
159
|
+
| 도구 | 하는 일 |
|
|
160
|
+
|------|--------|
|
|
161
|
+
| `bq_raw_sql` | SQL 직접 실행 (UNNEST, JOIN 등) |
|
|
162
|
+
| `bq_event_params` | event_params 쉽게 추출 |
|
|
163
|
+
| `bq_user_journey` | 사용자별 이벤트 시퀀스 분석 |
|
|
164
|
+
| `bq_funnel` | 전환 퍼널 자동 계산 |
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
73
168
|
## CLI 명령어
|
|
74
169
|
|
|
75
170
|
```bash
|
|
76
171
|
# 인터랙티브 메뉴
|
|
77
|
-
npx careerly-
|
|
172
|
+
npx careerly-data-mcp
|
|
78
173
|
|
|
79
|
-
# 초기 설정
|
|
80
|
-
npx careerly-
|
|
174
|
+
# 초기 설정
|
|
175
|
+
npx careerly-data-mcp setup
|
|
81
176
|
|
|
82
|
-
#
|
|
83
|
-
npx careerly-
|
|
177
|
+
# 연결 테스트
|
|
178
|
+
npx careerly-data-mcp test-connection
|
|
84
179
|
|
|
85
|
-
#
|
|
86
|
-
npx careerly-
|
|
180
|
+
# 서버 정보
|
|
181
|
+
npx careerly-data-mcp info
|
|
87
182
|
|
|
88
183
|
# MCP 서버 시작 (수동)
|
|
89
|
-
npx careerly-
|
|
184
|
+
npx careerly-data-mcp serve
|
|
90
185
|
```
|
|
91
186
|
|
|
92
|
-
|
|
187
|
+
> **별칭**: `careerly-ga4-mcp`, `careerly-ga4`도 동일하게 동작합니다.
|
|
93
188
|
|
|
94
|
-
|
|
95
|
-
npx careerly-ga4-mcp
|
|
96
|
-
```
|
|
189
|
+
---
|
|
97
190
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
191
|
+
## 문제 해결
|
|
192
|
+
|
|
193
|
+
### MCP 서버가 `/mcp`에 안 보여요
|
|
194
|
+
|
|
195
|
+
1. Claude Code 완전 종료 후 재시작
|
|
196
|
+
2. `claude mcp list`로 등록 확인
|
|
197
|
+
3. `npx careerly-data-mcp setup` 재실행
|
|
198
|
+
|
|
199
|
+
### PERMISSION_DENIED 에러
|
|
200
|
+
|
|
201
|
+
**GA4 권한 문제:**
|
|
202
|
+
- GA4 Admin > 속성 액세스 관리 > Service Account 이메일 추가
|
|
203
|
+
|
|
204
|
+
**BigQuery 권한 문제:**
|
|
205
|
+
- IAM & Admin > Service Account에 "BigQuery 데이터 뷰어" 역할 추가
|
|
101
206
|
|
|
102
|
-
|
|
103
|
-
Property ID: 123456789
|
|
104
|
-
Credentials: ~/project/service-account.json
|
|
105
|
-
Tools: 3 tools
|
|
207
|
+
### MCP 제거
|
|
106
208
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
209
|
+
```bash
|
|
210
|
+
# CLI로 삭제
|
|
211
|
+
claude mcp remove careerly-ga4 -s user
|
|
212
|
+
|
|
213
|
+
# 또는 메뉴에서
|
|
214
|
+
npx careerly-data-mcp
|
|
215
|
+
# → Disable 선택
|
|
112
216
|
```
|
|
113
217
|
|
|
114
|
-
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## 수동 설정 (선택)
|
|
115
221
|
|
|
116
222
|
### Claude CLI로 추가
|
|
117
223
|
|
|
@@ -119,112 +225,52 @@ Tools: 3 tools
|
|
|
119
225
|
claude mcp add careerly-ga4 \
|
|
120
226
|
-s user \
|
|
121
227
|
-e GA4_PROPERTY_ID=123456789 \
|
|
122
|
-
-e
|
|
123
|
-
|
|
228
|
+
-e BQ_PROJECT_ID=your-project-id \
|
|
229
|
+
-e GOOGLE_APPLICATION_CREDENTIALS=/path/to/key.json \
|
|
230
|
+
-- npx -y careerly-data-mcp serve
|
|
124
231
|
```
|
|
125
232
|
|
|
126
233
|
### 설정 파일 직접 수정
|
|
127
234
|
|
|
128
|
-
`~/.claude/settings.local.json
|
|
235
|
+
`~/.claude/settings.local.json`:
|
|
129
236
|
|
|
130
237
|
```json
|
|
131
238
|
{
|
|
132
239
|
"mcpServers": {
|
|
133
240
|
"careerly-ga4": {
|
|
134
241
|
"command": "npx",
|
|
135
|
-
"args": ["-y", "careerly-
|
|
242
|
+
"args": ["-y", "careerly-data-mcp", "serve"],
|
|
136
243
|
"env": {
|
|
137
244
|
"GA4_PROPERTY_ID": "123456789",
|
|
138
|
-
"
|
|
245
|
+
"BQ_PROJECT_ID": "your-project-id",
|
|
246
|
+
"GOOGLE_APPLICATION_CREDENTIALS": "/path/to/key.json"
|
|
139
247
|
}
|
|
140
248
|
}
|
|
141
249
|
}
|
|
142
250
|
}
|
|
143
251
|
```
|
|
144
252
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
### MCP 서버가 `/mcp`에 안 보여요
|
|
148
|
-
|
|
149
|
-
1. Claude Code를 완전히 종료 후 재시작
|
|
150
|
-
2. 설정 확인: `claude mcp list`
|
|
151
|
-
3. 재설정: `npx careerly-ga4-mcp setup`
|
|
152
|
-
|
|
153
|
-
### PERMISSION_DENIED 에러
|
|
154
|
-
|
|
155
|
-
GA4에 Service Account 권한이 없습니다:
|
|
156
|
-
|
|
157
|
-
1. GA4 Admin > 속성 설정 > 속성 액세스 관리
|
|
158
|
-
2. Service Account 이메일 추가 (JSON의 `client_email`)
|
|
159
|
-
3. 역할: 뷰어 (읽기 전용)
|
|
160
|
-
|
|
161
|
-
### 설정 삭제
|
|
162
|
-
|
|
163
|
-
```bash
|
|
164
|
-
# Claude CLI로 삭제
|
|
165
|
-
claude mcp remove careerly-ga4 -s user
|
|
166
|
-
|
|
167
|
-
# 또는 인터랙티브 메뉴에서
|
|
168
|
-
npx careerly-ga4-mcp
|
|
169
|
-
# → Disable 선택
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
## Tool 스키마
|
|
173
|
-
|
|
174
|
-
### ga4_query
|
|
175
|
-
|
|
176
|
-
```typescript
|
|
177
|
-
{
|
|
178
|
-
metrics: string[] // 필수: ["sessions", "totalUsers", "conversions"]
|
|
179
|
-
dimensions?: string[] // 선택: ["date", "sessionDefaultChannelGroup"]
|
|
180
|
-
startDate?: string // 기본: "7daysAgo"
|
|
181
|
-
endDate?: string // 기본: "today"
|
|
182
|
-
datePreset?: string // "last_7_days", "last_30_days" 등
|
|
183
|
-
limit?: number // 기본: 10
|
|
184
|
-
}
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
### 지표 (Metrics)
|
|
253
|
+
---
|
|
188
254
|
|
|
189
|
-
|
|
190
|
-
|------|------|
|
|
191
|
-
| `sessions` | 세션 수 |
|
|
192
|
-
| `totalUsers` | 전체 사용자 |
|
|
193
|
-
| `newUsers` | 신규 사용자 |
|
|
194
|
-
| `activeUsers` | 활성 사용자 |
|
|
195
|
-
| `bounceRate` | 이탈률 |
|
|
196
|
-
| `conversions` | 전환 수 |
|
|
197
|
-
| `screenPageViews` | 페이지뷰 |
|
|
198
|
-
| `engagementRate` | 참여율 |
|
|
255
|
+
## 보안 원칙
|
|
199
256
|
|
|
200
|
-
|
|
257
|
+
- `bq_raw_sql`은 **SELECT/WITH 문만 허용** (INSERT, DELETE 등 차단)
|
|
258
|
+
- Service Account 키 파일은 **개인 로컬에서만 관리**
|
|
259
|
+
- 민감 데이터 접근 시 권한 최소화 원칙 적용
|
|
201
260
|
|
|
202
|
-
|
|
203
|
-
|------|------|
|
|
204
|
-
| `date` | 날짜 |
|
|
205
|
-
| `sessionDefaultChannelGroup` | 채널 |
|
|
206
|
-
| `sessionSourceMedium` | 소스/매체 |
|
|
207
|
-
| `sessionCampaignName` | 캠페인 |
|
|
208
|
-
| `landingPage` | 랜딩 페이지 |
|
|
209
|
-
| `deviceCategory` | 디바이스 |
|
|
210
|
-
| `country` | 국가 |
|
|
261
|
+
---
|
|
211
262
|
|
|
212
263
|
## 개발
|
|
213
264
|
|
|
214
265
|
```bash
|
|
215
|
-
# 의존성 설치
|
|
216
|
-
npm
|
|
217
|
-
|
|
218
|
-
#
|
|
219
|
-
npm run build
|
|
220
|
-
|
|
221
|
-
# 개발 모드
|
|
222
|
-
npm run dev
|
|
223
|
-
|
|
224
|
-
# 타입 체크
|
|
225
|
-
npm run typecheck
|
|
266
|
+
npm install # 의존성 설치
|
|
267
|
+
npm run build # 빌드
|
|
268
|
+
npm run dev # 개발 모드 (watch)
|
|
269
|
+
npm run typecheck # 타입 체크
|
|
226
270
|
```
|
|
227
271
|
|
|
272
|
+
---
|
|
273
|
+
|
|
228
274
|
## 라이선스
|
|
229
275
|
|
|
230
276
|
MIT
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* BigQuery 클라이언트
|
|
3
3
|
* @google-cloud/bigquery 래퍼
|
|
4
4
|
*/
|
|
5
|
-
import type { BigQueryConfig, BigQueryRequest, BigQueryResponse, GA4EventsRequest, DatasetInfo, TableInfo } from "./types.js";
|
|
5
|
+
import type { BigQueryConfig, BigQueryRequest, BigQueryResponse, GA4EventsRequest, RawSqlRequest, DatasetInfo, TableInfo } from "./types.js";
|
|
6
6
|
export declare class BigQueryClient {
|
|
7
7
|
private client;
|
|
8
8
|
private projectId;
|
|
@@ -19,6 +19,11 @@ export declare class BigQueryClient {
|
|
|
19
19
|
* 파라미터 기반 쿼리 실행
|
|
20
20
|
*/
|
|
21
21
|
runQuery(request: BigQueryRequest): Promise<BigQueryResponse>;
|
|
22
|
+
/**
|
|
23
|
+
* Raw SQL 쿼리 실행 (고급 사용자용)
|
|
24
|
+
* 보안: SELECT 문만 허용, DML/DDL 차단
|
|
25
|
+
*/
|
|
26
|
+
runRawQuery(request: RawSqlRequest): Promise<BigQueryResponse>;
|
|
22
27
|
/**
|
|
23
28
|
* GA4 Export 이벤트 쿼리 실행
|
|
24
29
|
*/
|
|
@@ -42,6 +47,8 @@ export declare class BigQueryClient {
|
|
|
42
47
|
/**
|
|
43
48
|
* GA4 Export 쿼리 빌드
|
|
44
49
|
* CTE를 사용하여 event_params/user_properties 추출 후 집계
|
|
50
|
+
*
|
|
51
|
+
* 수정: GROUP BY 오류 해결 - metrics 유무에 따른 분기 처리 개선
|
|
45
52
|
*/
|
|
46
53
|
private buildGA4EventsQuery;
|
|
47
54
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/bigquery/client.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EACV,cAAc,EACd,eAAe,EACf,gBAAgB,EAChB,gBAAgB,EAChB,WAAW,EACX,SAAS,EAKV,MAAM,YAAY,CAAC;AAIpB,qBAAa,cAAc;IACzB,OAAO,CAAC,MAAM,CAAW;IACzB,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,QAAQ,CAAS;gBAEb,MAAM,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC;IAW5C;;OAEG;IACG,cAAc,IAAI,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAoBtE;;OAEG;IACG,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,gBAAgB,CAAC;IA+BnE;;OAEG;IACG,iBAAiB,CACrB,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,gBAAgB,CAAC;IA+B5B;;OAEG;IACG,YAAY,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;IAqB5C;;OAEG;IACG,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IAgCzD;;OAEG;IACG,cAAc,CAClB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,SAAS,CAAC;IAgCrB;;OAEG;IACH,OAAO,CAAC,gBAAgB;
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/bigquery/client.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EACV,cAAc,EACd,eAAe,EACf,gBAAgB,EAChB,gBAAgB,EAChB,aAAa,EACb,WAAW,EACX,SAAS,EAKV,MAAM,YAAY,CAAC;AAIpB,qBAAa,cAAc;IACzB,OAAO,CAAC,MAAM,CAAW;IACzB,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,QAAQ,CAAS;gBAEb,MAAM,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC;IAW5C;;OAEG;IACG,cAAc,IAAI,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAoBtE;;OAEG;IACG,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,gBAAgB,CAAC;IA+BnE;;;OAGG;IACG,WAAW,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,gBAAgB,CAAC;IA4DpE;;OAEG;IACG,iBAAiB,CACrB,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,gBAAgB,CAAC;IA+B5B;;OAEG;IACG,YAAY,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;IAqB5C;;OAEG;IACG,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IAgCzD;;OAEG;IACG,cAAc,CAClB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,SAAS,CAAC;IAgCrB;;OAEG;IACH,OAAO,CAAC,gBAAgB;IA6ExB;;;;;OAKG;IACH,OAAO,CAAC,mBAAmB;IA0L3B;;OAEG;IACH,OAAO,CAAC,yBAAyB;IAwCjC;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAmE3B;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAsBxB;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAWxB;;OAEG;IACH,OAAO,CAAC,eAAe;IAevB;;OAEG;IACH,OAAO,CAAC,WAAW;IAuDnB;;OAEG;IACH,YAAY,IAAI,MAAM;IAItB;;OAEG;IACH,WAAW,IAAI,MAAM;CAGtB;AAKD,wBAAgB,iBAAiB,CAC/B,MAAM,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,GAC/B,cAAc,CAKhB;AAED,wBAAgB,mBAAmB,IAAI,IAAI,CAE1C"}
|
package/dist/bigquery/client.js
CHANGED
|
@@ -68,6 +68,56 @@ export class BigQueryClient {
|
|
|
68
68
|
throw this.handleError(error);
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* Raw SQL 쿼리 실행 (고급 사용자용)
|
|
73
|
+
* 보안: SELECT 문만 허용, DML/DDL 차단
|
|
74
|
+
*/
|
|
75
|
+
async runRawQuery(request) {
|
|
76
|
+
try {
|
|
77
|
+
// SQL 검증: SELECT 문만 허용
|
|
78
|
+
const normalizedSql = request.sql.trim().toUpperCase();
|
|
79
|
+
if (!normalizedSql.startsWith("SELECT") && !normalizedSql.startsWith("WITH")) {
|
|
80
|
+
throw new BigQueryError("Raw SQL은 SELECT 또는 WITH 문만 지원합니다. INSERT, UPDATE, DELETE, DROP 등은 사용할 수 없습니다.", "INVALID_QUERY");
|
|
81
|
+
}
|
|
82
|
+
// 위험한 키워드 차단
|
|
83
|
+
const dangerousKeywords = [
|
|
84
|
+
"INSERT", "UPDATE", "DELETE", "DROP", "TRUNCATE",
|
|
85
|
+
"CREATE", "ALTER", "GRANT", "REVOKE", "MERGE"
|
|
86
|
+
];
|
|
87
|
+
for (const keyword of dangerousKeywords) {
|
|
88
|
+
// 문장 시작에 있는 경우만 차단 (서브쿼리 내부는 허용)
|
|
89
|
+
if (normalizedSql.match(new RegExp(`^\\s*${keyword}\\s`, "i"))) {
|
|
90
|
+
throw new BigQueryError(`${keyword} 문은 지원하지 않습니다. SELECT 문만 사용해주세요.`, "INVALID_QUERY");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// LIMIT 자동 추가 (없으면 기본값 적용)
|
|
94
|
+
let finalSql = request.sql.trim();
|
|
95
|
+
if (!normalizedSql.includes("LIMIT")) {
|
|
96
|
+
const limit = Math.min(request.limit || 1000, 10000);
|
|
97
|
+
finalSql = `${finalSql}\nLIMIT ${limit}`;
|
|
98
|
+
}
|
|
99
|
+
const [job] = await this.client.createQueryJob({
|
|
100
|
+
query: finalSql,
|
|
101
|
+
params: request.params,
|
|
102
|
+
location: this.location,
|
|
103
|
+
useLegacySql: false,
|
|
104
|
+
});
|
|
105
|
+
const [rows] = await job.getQueryResults();
|
|
106
|
+
const [metadata] = await job.getMetadata();
|
|
107
|
+
const headers = rows.length > 0 ? Object.keys(rows[0]) : [];
|
|
108
|
+
return {
|
|
109
|
+
headers,
|
|
110
|
+
rows: rows,
|
|
111
|
+
totalRows: rows.length,
|
|
112
|
+
jobId: job.id,
|
|
113
|
+
bytesProcessed: Number(metadata.statistics?.totalBytesProcessed || 0),
|
|
114
|
+
cacheHit: metadata.statistics?.query?.cacheHit || false,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
throw this.handleError(error);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
71
121
|
/**
|
|
72
122
|
* GA4 Export 이벤트 쿼리 실행
|
|
73
123
|
*/
|
|
@@ -215,14 +265,15 @@ export class BigQueryClient {
|
|
|
215
265
|
const projectId = request.projectId || this.projectId;
|
|
216
266
|
const tableRef = `\`${projectId}.${request.datasetId}.${request.tableId}\``;
|
|
217
267
|
parts.push(`FROM ${tableRef}`);
|
|
218
|
-
// WHERE
|
|
268
|
+
// WHERE (복합 필터 로직 지원)
|
|
219
269
|
if (request.where?.length) {
|
|
220
270
|
const conditions = request.where.map((w) => {
|
|
221
271
|
const result = this.buildWhereCondition(w, paramIndex, params);
|
|
222
272
|
paramIndex = result.nextIndex;
|
|
223
273
|
return result.condition;
|
|
224
274
|
});
|
|
225
|
-
|
|
275
|
+
const logic = request.filterLogic || "AND";
|
|
276
|
+
parts.push(`WHERE ${conditions.join(` ${logic} `)}`);
|
|
226
277
|
}
|
|
227
278
|
// GROUP BY
|
|
228
279
|
if (request.groupBy?.length) {
|
|
@@ -250,12 +301,16 @@ export class BigQueryClient {
|
|
|
250
301
|
/**
|
|
251
302
|
* GA4 Export 쿼리 빌드
|
|
252
303
|
* CTE를 사용하여 event_params/user_properties 추출 후 집계
|
|
304
|
+
*
|
|
305
|
+
* 수정: GROUP BY 오류 해결 - metrics 유무에 따른 분기 처리 개선
|
|
253
306
|
*/
|
|
254
307
|
buildGA4EventsQuery(request) {
|
|
255
308
|
const params = {};
|
|
256
309
|
const projectId = request.projectId || this.projectId;
|
|
257
310
|
const tableRef = `\`${projectId}.${request.datasetId}.events_*\``;
|
|
258
311
|
let paramIndex = 0;
|
|
312
|
+
// metrics 유무에 따른 집계 모드 결정
|
|
313
|
+
const isAggregateMode = !!(request.metrics?.length);
|
|
259
314
|
// WHERE 조건 구성
|
|
260
315
|
const whereConditions = [];
|
|
261
316
|
whereConditions.push(`_TABLE_SUFFIX BETWEEN '${request.startDate}' AND '${request.endDate}'`);
|
|
@@ -314,77 +369,88 @@ export class BigQueryClient {
|
|
|
314
369
|
FROM ${tableRef}
|
|
315
370
|
${whereClause}
|
|
316
371
|
)`;
|
|
317
|
-
//
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
372
|
+
// paramFilters 처리 (추출된 event_params에 대한 필터)
|
|
373
|
+
const mainWhereConditions = [];
|
|
374
|
+
if (request.paramFilters?.length) {
|
|
375
|
+
request.paramFilters.forEach((pf, idx) => {
|
|
376
|
+
const condition = this.buildParamFilterCondition(pf, paramIndex + idx, params);
|
|
377
|
+
if (condition) {
|
|
378
|
+
mainWhereConditions.push(condition.condition);
|
|
379
|
+
paramIndex = condition.nextIndex;
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
// 집계 모드: GROUP BY 사용
|
|
384
|
+
if (isAggregateMode) {
|
|
385
|
+
const mainSelectParts = [];
|
|
386
|
+
const groupByFields = [];
|
|
387
|
+
// event_name은 항상 GROUP BY에 포함
|
|
388
|
+
mainSelectParts.push("event_name");
|
|
389
|
+
groupByFields.push("event_name");
|
|
390
|
+
// dimensions는 GROUP BY에 포함
|
|
391
|
+
if (request.dimensions?.length) {
|
|
392
|
+
request.dimensions.forEach((dim) => {
|
|
393
|
+
const alias = dim.includes(".") ? dim.replace(/\./g, "_") : dim;
|
|
394
|
+
mainSelectParts.push(alias);
|
|
395
|
+
groupByFields.push(alias);
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
// eventParams는 집계 모드에서 GROUP BY에 포함 안함 (집계 결과만)
|
|
399
|
+
// 대신 첫 번째 값만 보여주거나 제외
|
|
400
|
+
// 참고: 만약 eventParams를 GROUP BY에 포함하려면 dimensions에 추가해야 함
|
|
401
|
+
// metrics (집계)
|
|
402
|
+
request.metrics.forEach((agg) => {
|
|
403
|
+
mainSelectParts.push(this.buildAggregation(agg));
|
|
404
|
+
});
|
|
405
|
+
// 기본 이벤트 카운트도 추가
|
|
406
|
+
mainSelectParts.push("COUNT(*) AS event_count");
|
|
407
|
+
// 메인 쿼리 구성
|
|
408
|
+
let mainQuery = `SELECT
|
|
409
|
+
${mainSelectParts.join(",\n ")}
|
|
410
|
+
FROM extracted_events`;
|
|
411
|
+
if (mainWhereConditions.length > 0) {
|
|
412
|
+
mainQuery += `\nWHERE ${mainWhereConditions.join("\n AND ")}`;
|
|
413
|
+
}
|
|
414
|
+
mainQuery += `\nGROUP BY ${groupByFields.join(", ")}
|
|
415
|
+
ORDER BY event_count DESC
|
|
416
|
+
LIMIT ${Math.min(request.limit || 100, 10000)}`;
|
|
417
|
+
return { query: `${cte}\n${mainQuery}`, params };
|
|
418
|
+
}
|
|
419
|
+
// 비집계 모드: raw 데이터 반환 (GROUP BY 없음)
|
|
420
|
+
const mainSelectParts = [
|
|
421
|
+
"event_name",
|
|
422
|
+
"event_timestamp",
|
|
423
|
+
"user_pseudo_id",
|
|
424
|
+
];
|
|
425
|
+
// eventParams 포함
|
|
325
426
|
if (request.eventParams?.length) {
|
|
326
427
|
request.eventParams.forEach((paramKey) => {
|
|
327
|
-
|
|
328
|
-
mainSelectParts.push(safeKey);
|
|
329
|
-
// metrics가 없으면 GROUP BY에 포함
|
|
330
|
-
if (!request.metrics?.length) {
|
|
331
|
-
groupByParts.push(String(selectIndex));
|
|
332
|
-
}
|
|
333
|
-
selectIndex++;
|
|
428
|
+
mainSelectParts.push(this.escapeIdentifier(paramKey));
|
|
334
429
|
});
|
|
335
430
|
}
|
|
336
|
-
//
|
|
431
|
+
// userProperties 포함
|
|
337
432
|
if (request.userProperties?.length) {
|
|
338
433
|
request.userProperties.forEach((propKey) => {
|
|
339
|
-
|
|
340
|
-
mainSelectParts.push(`user_${safeKey}`);
|
|
341
|
-
if (!request.metrics?.length) {
|
|
342
|
-
groupByParts.push(String(selectIndex));
|
|
343
|
-
}
|
|
344
|
-
selectIndex++;
|
|
434
|
+
mainSelectParts.push(`user_${this.escapeIdentifier(propKey)}`);
|
|
345
435
|
});
|
|
346
436
|
}
|
|
347
|
-
// dimensions
|
|
437
|
+
// dimensions 포함
|
|
348
438
|
if (request.dimensions?.length) {
|
|
349
439
|
request.dimensions.forEach((dim) => {
|
|
350
440
|
const alias = dim.includes(".") ? dim.replace(/\./g, "_") : dim;
|
|
351
441
|
mainSelectParts.push(alias);
|
|
352
|
-
groupByParts.push(String(selectIndex++));
|
|
353
442
|
});
|
|
354
443
|
}
|
|
355
|
-
//
|
|
356
|
-
if (request.metrics?.length) {
|
|
357
|
-
request.metrics.forEach((agg) => {
|
|
358
|
-
mainSelectParts.push(this.buildAggregation(agg));
|
|
359
|
-
});
|
|
360
|
-
}
|
|
361
|
-
else {
|
|
362
|
-
// 기본 집계: 이벤트 수
|
|
363
|
-
mainSelectParts.push("COUNT(*) AS event_count");
|
|
364
|
-
}
|
|
365
|
-
// paramFilters 처리 (추출된 event_params에 대한 필터)
|
|
366
|
-
const mainWhereConditions = [];
|
|
367
|
-
if (request.paramFilters?.length) {
|
|
368
|
-
request.paramFilters.forEach((pf, idx) => {
|
|
369
|
-
const condition = this.buildParamFilterCondition(pf, paramIndex + idx, params);
|
|
370
|
-
if (condition) {
|
|
371
|
-
mainWhereConditions.push(condition.condition);
|
|
372
|
-
paramIndex = condition.nextIndex;
|
|
373
|
-
}
|
|
374
|
-
});
|
|
375
|
-
}
|
|
376
|
-
// 메인 쿼리 구성
|
|
444
|
+
// 메인 쿼리 구성 (GROUP BY 없음)
|
|
377
445
|
let mainQuery = `SELECT
|
|
378
446
|
${mainSelectParts.join(",\n ")}
|
|
379
447
|
FROM extracted_events`;
|
|
380
448
|
if (mainWhereConditions.length > 0) {
|
|
381
449
|
mainQuery += `\nWHERE ${mainWhereConditions.join("\n AND ")}`;
|
|
382
450
|
}
|
|
383
|
-
mainQuery += `\
|
|
384
|
-
ORDER BY event_count DESC
|
|
451
|
+
mainQuery += `\nORDER BY event_timestamp DESC
|
|
385
452
|
LIMIT ${Math.min(request.limit || 100, 10000)}`;
|
|
386
|
-
|
|
387
|
-
return { query, params };
|
|
453
|
+
return { query: `${cte}\n${mainQuery}`, params };
|
|
388
454
|
}
|
|
389
455
|
/**
|
|
390
456
|
* event_params 필터 조건 빌드
|