@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.
@@ -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
- firstPage = false;
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") flush();
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sayren/storefront-sdk",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "main": "./dist/index.mjs",
5
5
  "types": "./dist/index.d.mts",
6
6
  "exports": {
@@ -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
- const push = (event: Pending, dedupeKey: string) => {
158
- if (!enabled || consent === "denied") return;
159
- if (seen.has(dedupeKey) || !allowRate(event.name)) return;
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
- firstPage = false;
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") flush();
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,