@sayren/storefront-sdk 0.2.0 → 0.2.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/dist/analytics/index.mjs +29 -7
- package/package.json +1 -1
- package/src/analytics/index.ts +32 -8
package/dist/analytics/index.mjs
CHANGED
|
@@ -7,6 +7,11 @@ const SESSION_CAMPAIGN_COOKIE = "sy_scmp";
|
|
|
7
7
|
const MAX_BATCH = 50;
|
|
8
8
|
/** 이벤트 이름당 분당 상한 — 무한 루프 버그가 트래픽을 만들지 못하게 */
|
|
9
9
|
const PER_NAME_PER_MINUTE = 30;
|
|
10
|
+
/**
|
|
11
|
+
* 전체 분당 상한 — 서버의 방문자당 분당 한도(120)보다 낮게 둬, SDK가 보낸 이벤트가 서버 한도에 걸려
|
|
12
|
+
* 조용히 버려지지 않게 한다(이름당 상한만으로는 이름 5종 × 30 = 150까지 갈 수 있다).
|
|
13
|
+
*/
|
|
14
|
+
const TOTAL_PER_MINUTE = 100;
|
|
10
15
|
const MAX_QUEUE = 500;
|
|
11
16
|
const DEV_HOST = /^(localhost|127\.\d+\.\d+\.\d+|\[?::1\]?|0\.0\.0\.0|10\.\d+\.\d+\.\d+|192\.168\.\d+\.\d+|172\.(1[6-9]|2\d|3[01])\.\d+\.\d+)$|\.local$/i;
|
|
12
17
|
function createAnalytics(options) {
|
|
@@ -27,9 +32,15 @@ function createAnalyticsWith(options, env) {
|
|
|
27
32
|
let visibleMs = 0;
|
|
28
33
|
let engagementSent = false;
|
|
29
34
|
let firstPage = true;
|
|
35
|
+
/**
|
|
36
|
+
* 지금 페이지뷰를 보냈는가 — 상한으로 페이지뷰가 버려졌으면 그 페이지의 상품 조회·목록·검색·체류도 보내지 않는다.
|
|
37
|
+
* 그러지 않으면 존재하지 않는 페이지뷰를 가리키는 이벤트가 남는다.
|
|
38
|
+
*/
|
|
39
|
+
let pageAccepted = false;
|
|
30
40
|
const seen = /* @__PURE__ */ new Set();
|
|
31
41
|
let rateMinute = -1;
|
|
32
42
|
const perName = /* @__PURE__ */ new Map();
|
|
43
|
+
let totalThisMinute = 0;
|
|
33
44
|
const cleanups = [];
|
|
34
45
|
const campaignOf = (href) => {
|
|
35
46
|
try {
|
|
@@ -64,19 +75,23 @@ function createAnalyticsWith(options, env) {
|
|
|
64
75
|
if (minute !== rateMinute) {
|
|
65
76
|
rateMinute = minute;
|
|
66
77
|
perName.clear();
|
|
78
|
+
totalThisMinute = 0;
|
|
67
79
|
}
|
|
68
80
|
const used = perName.get(name) ?? 0;
|
|
69
|
-
if (used >= PER_NAME_PER_MINUTE) return false;
|
|
81
|
+
if (used >= PER_NAME_PER_MINUTE || totalThisMinute >= TOTAL_PER_MINUTE) return false;
|
|
70
82
|
perName.set(name, used + 1);
|
|
83
|
+
totalThisMinute += 1;
|
|
71
84
|
return true;
|
|
72
85
|
};
|
|
86
|
+
/** 큐에 넣었으면 true — 중복·상한·거부로 버리면 false */
|
|
73
87
|
const push = (event, dedupeKey) => {
|
|
74
|
-
if (!enabled || consent === "denied") return;
|
|
75
|
-
if (seen.has(dedupeKey) || !allowRate(event.name)) return;
|
|
88
|
+
if (!enabled || consent === "denied") return false;
|
|
89
|
+
if (seen.has(dedupeKey) || !allowRate(event.name)) return false;
|
|
76
90
|
seen.add(dedupeKey);
|
|
77
91
|
if (queue.length >= MAX_QUEUE) queue.shift();
|
|
78
92
|
queue.push(event);
|
|
79
93
|
if (queue.length >= MAX_BATCH) flush();
|
|
94
|
+
return true;
|
|
80
95
|
};
|
|
81
96
|
const flush = () => {
|
|
82
97
|
if (!enabled || queue.length === 0) return;
|
|
@@ -96,7 +111,7 @@ function createAnalyticsWith(options, env) {
|
|
|
96
111
|
const occurredAt = () => new Date(env.now()).toISOString();
|
|
97
112
|
/** 지금 페이지의 체류를 한 번 보낸다(이동·숨김·닫힘 중 먼저 오는 때) */
|
|
98
113
|
const endPage = () => {
|
|
99
|
-
if (!pageViewId || engagementSent) return;
|
|
114
|
+
if (!pageViewId || engagementSent || !pageAccepted) return;
|
|
100
115
|
if (visibleSince !== null) {
|
|
101
116
|
visibleMs += env.now() - visibleSince;
|
|
102
117
|
visibleSince = env.visible() ? env.now() : null;
|
|
@@ -126,14 +141,14 @@ function createAnalyticsWith(options, env) {
|
|
|
126
141
|
visibleSince = env.visible() ? env.now() : null;
|
|
127
142
|
engagementSent = false;
|
|
128
143
|
const referrer = firstPage ? env.referrer() : "";
|
|
129
|
-
|
|
130
|
-
push({
|
|
144
|
+
pageAccepted = push({
|
|
131
145
|
name: "page_view",
|
|
132
146
|
occurredAt: occurredAt(),
|
|
133
147
|
url: href,
|
|
134
148
|
pageViewId,
|
|
135
149
|
...referrer ? { referrer } : {}
|
|
136
150
|
}, `page:${pageViewId}`);
|
|
151
|
+
if (pageAccepted) firstPage = false;
|
|
137
152
|
};
|
|
138
153
|
/** 프리렌더·숨은 탭은 화면에 보이는 순간 첫 페이지뷰를 보낸다 */
|
|
139
154
|
const startWhenVisible = () => {
|
|
@@ -164,6 +179,7 @@ function createAnalyticsWith(options, env) {
|
|
|
164
179
|
return {
|
|
165
180
|
track(input) {
|
|
166
181
|
if (!pageViewId) startPage();
|
|
182
|
+
if (!pageAccepted) return;
|
|
167
183
|
const common = {
|
|
168
184
|
occurredAt: occurredAt(),
|
|
169
185
|
url: env.href(),
|
|
@@ -198,7 +214,13 @@ function createAnalyticsWith(options, env) {
|
|
|
198
214
|
setConsent(next) {
|
|
199
215
|
consent = next;
|
|
200
216
|
if (next === "denied") queue = [];
|
|
201
|
-
if (next === "granted")
|
|
217
|
+
if (next === "granted") {
|
|
218
|
+
if (pageViewId && !pageAccepted && options.autoPageViews !== false) {
|
|
219
|
+
pageKey = "";
|
|
220
|
+
startPage();
|
|
221
|
+
}
|
|
222
|
+
flush();
|
|
223
|
+
}
|
|
202
224
|
},
|
|
203
225
|
ids: () => enabled ? ensureIds() : null,
|
|
204
226
|
flush,
|
package/package.json
CHANGED
package/src/analytics/index.ts
CHANGED
|
@@ -84,6 +84,11 @@ const SESSION_CAMPAIGN_COOKIE = "sy_scmp";
|
|
|
84
84
|
const MAX_BATCH = 50;
|
|
85
85
|
/** 이벤트 이름당 분당 상한 — 무한 루프 버그가 트래픽을 만들지 못하게 */
|
|
86
86
|
const PER_NAME_PER_MINUTE = 30;
|
|
87
|
+
/**
|
|
88
|
+
* 전체 분당 상한 — 서버의 방문자당 분당 한도(120)보다 낮게 둬, SDK가 보낸 이벤트가 서버 한도에 걸려
|
|
89
|
+
* 조용히 버려지지 않게 한다(이름당 상한만으로는 이름 5종 × 30 = 150까지 갈 수 있다).
|
|
90
|
+
*/
|
|
91
|
+
const TOTAL_PER_MINUTE = 100;
|
|
87
92
|
const MAX_QUEUE = 500;
|
|
88
93
|
|
|
89
94
|
const DEV_HOST =
|
|
@@ -110,9 +115,15 @@ export function createAnalyticsWith(options: AnalyticsOptions, env: AnalyticsEnv
|
|
|
110
115
|
let visibleMs = 0;
|
|
111
116
|
let engagementSent = false;
|
|
112
117
|
let firstPage = true;
|
|
118
|
+
/**
|
|
119
|
+
* 지금 페이지뷰를 보냈는가 — 상한으로 페이지뷰가 버려졌으면 그 페이지의 상품 조회·목록·검색·체류도 보내지 않는다.
|
|
120
|
+
* 그러지 않으면 존재하지 않는 페이지뷰를 가리키는 이벤트가 남는다.
|
|
121
|
+
*/
|
|
122
|
+
let pageAccepted = false;
|
|
113
123
|
const seen = new Set<string>();
|
|
114
124
|
let rateMinute = -1;
|
|
115
125
|
const perName = new Map<string, number>();
|
|
126
|
+
let totalThisMinute = 0;
|
|
116
127
|
const cleanups: Array<() => void> = [];
|
|
117
128
|
|
|
118
129
|
const campaignOf = (href: string) => {
|
|
@@ -147,20 +158,24 @@ export function createAnalyticsWith(options: AnalyticsOptions, env: AnalyticsEnv
|
|
|
147
158
|
if (minute !== rateMinute) {
|
|
148
159
|
rateMinute = minute;
|
|
149
160
|
perName.clear();
|
|
161
|
+
totalThisMinute = 0;
|
|
150
162
|
}
|
|
151
163
|
const used = perName.get(name) ?? 0;
|
|
152
|
-
if (used >= PER_NAME_PER_MINUTE) return false;
|
|
164
|
+
if (used >= PER_NAME_PER_MINUTE || totalThisMinute >= TOTAL_PER_MINUTE) return false;
|
|
153
165
|
perName.set(name, used + 1);
|
|
166
|
+
totalThisMinute += 1;
|
|
154
167
|
return true;
|
|
155
168
|
};
|
|
156
169
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
if (
|
|
170
|
+
/** 큐에 넣었으면 true — 중복·상한·거부로 버리면 false */
|
|
171
|
+
const push = (event: Pending, dedupeKey: string): boolean => {
|
|
172
|
+
if (!enabled || consent === "denied") return false;
|
|
173
|
+
if (seen.has(dedupeKey) || !allowRate(event.name)) return false;
|
|
160
174
|
seen.add(dedupeKey);
|
|
161
175
|
if (queue.length >= MAX_QUEUE) queue.shift();
|
|
162
176
|
queue.push(event);
|
|
163
177
|
if (queue.length >= MAX_BATCH) flush();
|
|
178
|
+
return true;
|
|
164
179
|
};
|
|
165
180
|
|
|
166
181
|
const flush = () => {
|
|
@@ -183,7 +198,7 @@ export function createAnalyticsWith(options: AnalyticsOptions, env: AnalyticsEnv
|
|
|
183
198
|
|
|
184
199
|
/** 지금 페이지의 체류를 한 번 보낸다(이동·숨김·닫힘 중 먼저 오는 때) */
|
|
185
200
|
const endPage = () => {
|
|
186
|
-
if (!pageViewId || engagementSent) return;
|
|
201
|
+
if (!pageViewId || engagementSent || !pageAccepted) return;
|
|
187
202
|
if (visibleSince !== null) {
|
|
188
203
|
visibleMs += env.now() - visibleSince;
|
|
189
204
|
visibleSince = env.visible() ? env.now() : null;
|
|
@@ -219,8 +234,7 @@ export function createAnalyticsWith(options: AnalyticsOptions, env: AnalyticsEnv
|
|
|
219
234
|
visibleSince = env.visible() ? env.now() : null;
|
|
220
235
|
engagementSent = false;
|
|
221
236
|
const referrer = firstPage ? env.referrer() : "";
|
|
222
|
-
|
|
223
|
-
push(
|
|
237
|
+
pageAccepted = push(
|
|
224
238
|
{
|
|
225
239
|
name: "page_view",
|
|
226
240
|
occurredAt: occurredAt(),
|
|
@@ -230,6 +244,8 @@ export function createAnalyticsWith(options: AnalyticsOptions, env: AnalyticsEnv
|
|
|
230
244
|
},
|
|
231
245
|
`page:${pageViewId}`,
|
|
232
246
|
);
|
|
247
|
+
// 외부 유입(리퍼러)은 실제로 보낸 첫 페이지뷰에만 싣는다 — 거부 중 버린 페이지뷰가 가져가지 않게
|
|
248
|
+
if (pageAccepted) firstPage = false;
|
|
233
249
|
};
|
|
234
250
|
|
|
235
251
|
/** 프리렌더·숨은 탭은 화면에 보이는 순간 첫 페이지뷰를 보낸다 */
|
|
@@ -268,6 +284,7 @@ export function createAnalyticsWith(options: AnalyticsOptions, env: AnalyticsEnv
|
|
|
268
284
|
return {
|
|
269
285
|
track(input) {
|
|
270
286
|
if (!pageViewId) startPage();
|
|
287
|
+
if (!pageAccepted) return; // 페이지뷰가 버려진 페이지의 행동은 보내지 않는다
|
|
271
288
|
const common = { occurredAt: occurredAt(), url: env.href(), pageViewId };
|
|
272
289
|
switch (input.name) {
|
|
273
290
|
case "product_view":
|
|
@@ -308,7 +325,14 @@ export function createAnalyticsWith(options: AnalyticsOptions, env: AnalyticsEnv
|
|
|
308
325
|
setConsent(next) {
|
|
309
326
|
consent = next;
|
|
310
327
|
if (next === "denied") queue = [];
|
|
311
|
-
if (next === "granted")
|
|
328
|
+
if (next === "granted") {
|
|
329
|
+
// 거부 중에 연 페이지는 페이지뷰가 없다 — 동의한 지금 그 페이지를 새로 센다
|
|
330
|
+
if (pageViewId && !pageAccepted && options.autoPageViews !== false) {
|
|
331
|
+
pageKey = "";
|
|
332
|
+
startPage();
|
|
333
|
+
}
|
|
334
|
+
flush();
|
|
335
|
+
}
|
|
312
336
|
},
|
|
313
337
|
ids: () => (enabled ? ensureIds() : null),
|
|
314
338
|
flush,
|