@vircle/plugin-ecommerce-core 0.1.0

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/index.mjs ADDED
@@ -0,0 +1,401 @@
1
+ // src/NetworkInterceptor.ts
2
+ var NetworkInterceptor = class {
3
+ constructor(patterns, callback) {
4
+ this.patterns = [];
5
+ this.isActive = false;
6
+ this.patterns = patterns;
7
+ this.callback = callback;
8
+ }
9
+ /**
10
+ * Start intercepting network requests
11
+ */
12
+ start() {
13
+ if (this.isActive) return;
14
+ if (typeof window === "undefined") return;
15
+ this.patchFetch();
16
+ this.patchXHR();
17
+ this.isActive = true;
18
+ }
19
+ /**
20
+ * Stop intercepting and restore original functions
21
+ */
22
+ stop() {
23
+ if (!this.isActive) return;
24
+ this.restoreFetch();
25
+ this.restoreXHR();
26
+ this.isActive = false;
27
+ }
28
+ /**
29
+ * Check if a URL matches any registered pattern
30
+ */
31
+ matchUrl(url, method) {
32
+ return this.patterns.find((p) => {
33
+ const urlMatch = p.pattern instanceof RegExp ? p.pattern.test(url) : url.includes(p.pattern);
34
+ const methodMatch = !p.method || p.method === method?.toUpperCase();
35
+ return urlMatch && methodMatch;
36
+ });
37
+ }
38
+ patchFetch() {
39
+ if (typeof window.fetch !== "function") return;
40
+ this.originalFetch = window.fetch;
41
+ const self = this;
42
+ window.fetch = function(input, init) {
43
+ const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
44
+ const method = init?.method || "GET";
45
+ const pattern = self.matchUrl(url, method);
46
+ if (pattern && self.callback.onRequest) {
47
+ try {
48
+ let body;
49
+ try {
50
+ if (init?.body) {
51
+ body = typeof init.body === "string" ? init.body : init.body instanceof FormData ? Object.fromEntries(init.body.entries()) : init.body;
52
+ }
53
+ } catch {
54
+ }
55
+ self.callback.onRequest(url, method, body);
56
+ } catch {
57
+ }
58
+ }
59
+ return self.originalFetch.call(window, input, init).then((response) => {
60
+ if (pattern && self.callback.onResponse) {
61
+ try {
62
+ const contentType = response.headers.get("content-type") || "";
63
+ if (!contentType.includes("application/json") && !contentType.includes("text/")) {
64
+ try {
65
+ self.callback.onResponse(url, response.status, void 0);
66
+ } catch {
67
+ }
68
+ } else {
69
+ const cloned = response.clone();
70
+ cloned.text().then((text) => {
71
+ try {
72
+ const json = JSON.parse(text);
73
+ self.callback.onResponse(url, response.status, json);
74
+ } catch {
75
+ try {
76
+ self.callback.onResponse(url, response.status, text);
77
+ } catch {
78
+ }
79
+ }
80
+ }).catch(() => {
81
+ try {
82
+ self.callback.onResponse(url, response.status, void 0);
83
+ } catch {
84
+ }
85
+ });
86
+ }
87
+ } catch {
88
+ }
89
+ }
90
+ return response;
91
+ });
92
+ };
93
+ }
94
+ patchXHR() {
95
+ if (typeof XMLHttpRequest === "undefined") return;
96
+ this.originalXHROpen = XMLHttpRequest.prototype.open;
97
+ this.originalXHRSend = XMLHttpRequest.prototype.send;
98
+ const self = this;
99
+ XMLHttpRequest.prototype.open = function(method, url, ...args) {
100
+ this.__vircle_url = typeof url === "string" ? url : url.toString();
101
+ this.__vircle_method = method;
102
+ return self.originalXHROpen.apply(this, [method, url, ...args]);
103
+ };
104
+ XMLHttpRequest.prototype.send = function(body) {
105
+ const url = this.__vircle_url;
106
+ const method = this.__vircle_method;
107
+ const pattern = url ? self.matchUrl(url, method) : void 0;
108
+ if (pattern && self.callback.onRequest) {
109
+ try {
110
+ let parsedBody;
111
+ try {
112
+ if (typeof body === "string") {
113
+ parsedBody = body;
114
+ } else if (body instanceof FormData) {
115
+ parsedBody = Object.fromEntries(body.entries());
116
+ }
117
+ } catch {
118
+ }
119
+ self.callback.onRequest(url, method, parsedBody);
120
+ } catch {
121
+ }
122
+ }
123
+ if (pattern && self.callback.onResponse) {
124
+ this.addEventListener("load", function() {
125
+ try {
126
+ const contentType = this.getResponseHeader("content-type") || "";
127
+ if (!contentType.includes("application/json") && !contentType.includes("text/")) {
128
+ self.callback.onResponse(url, this.status, void 0);
129
+ return;
130
+ }
131
+ const responseBody = this.responseType === "" || this.responseType === "text" ? JSON.parse(this.responseText) : this.response;
132
+ self.callback.onResponse(url, this.status, responseBody);
133
+ } catch {
134
+ try {
135
+ self.callback.onResponse(url, this.status, this.responseText);
136
+ } catch {
137
+ }
138
+ }
139
+ });
140
+ }
141
+ return self.originalXHRSend.call(this, body);
142
+ };
143
+ }
144
+ restoreFetch() {
145
+ if (this.originalFetch && typeof window !== "undefined") {
146
+ window.fetch = this.originalFetch;
147
+ this.originalFetch = void 0;
148
+ }
149
+ }
150
+ restoreXHR() {
151
+ if (typeof XMLHttpRequest === "undefined") return;
152
+ if (this.originalXHROpen) {
153
+ XMLHttpRequest.prototype.open = this.originalXHROpen;
154
+ this.originalXHROpen = void 0;
155
+ }
156
+ if (this.originalXHRSend) {
157
+ XMLHttpRequest.prototype.send = this.originalXHRSend;
158
+ this.originalXHRSend = void 0;
159
+ }
160
+ }
161
+ };
162
+
163
+ // src/types.ts
164
+ var EcommerceEvent = /* @__PURE__ */ ((EcommerceEvent2) => {
165
+ EcommerceEvent2["PAGE_VIEWED"] = "page_viewed";
166
+ EcommerceEvent2["PRODUCT_VIEWED"] = "product_viewed";
167
+ EcommerceEvent2["PRODUCT_ADDED_TO_CART"] = "product_added_to_cart";
168
+ EcommerceEvent2["PRODUCT_REMOVED_FROM_CART"] = "product_removed_from_cart";
169
+ EcommerceEvent2["CART_VIEWED"] = "cart_viewed";
170
+ EcommerceEvent2["CART_UPDATED"] = "cart_updated";
171
+ EcommerceEvent2["PRODUCT_ADDED_TO_WISHLIST"] = "product_added_to_wishlist";
172
+ EcommerceEvent2["CHECKOUT_STARTED"] = "checkout_started";
173
+ EcommerceEvent2["EXTERNAL_PAYMENT_INITIATED"] = "external_payment_initiated";
174
+ EcommerceEvent2["ORDER_COMPLETED"] = "order_completed";
175
+ return EcommerceEvent2;
176
+ })(EcommerceEvent || {});
177
+ var PageType = /* @__PURE__ */ ((PageType2) => {
178
+ PageType2["HOME"] = "home";
179
+ PageType2["PRODUCT_LIST"] = "product_list";
180
+ PageType2["PRODUCT_DETAIL"] = "product_detail";
181
+ PageType2["CART"] = "cart";
182
+ PageType2["CHECKOUT"] = "checkout";
183
+ PageType2["ORDER_COMPLETE"] = "order_complete";
184
+ PageType2["SEARCH_RESULTS"] = "search_results";
185
+ PageType2["MY_PAGE"] = "my_page";
186
+ PageType2["OTHER"] = "other";
187
+ return PageType2;
188
+ })(PageType || {});
189
+
190
+ // src/BaseEcommercePlugin.ts
191
+ var DEFAULT_DEDUPLICATION_WINDOW = 2e3;
192
+ var CAMPAIGN_PARAMS = [
193
+ "utm_source",
194
+ "utm_medium",
195
+ "utm_campaign",
196
+ "utm_term",
197
+ "utm_content",
198
+ "gclid",
199
+ "fbclid",
200
+ "msclkid",
201
+ "ttclid"
202
+ ];
203
+ var CAMPAIGN_STORAGE_PREFIX = "__vircle_campaign_";
204
+ var BaseEcommercePlugin = class {
205
+ constructor() {
206
+ this.pluginConfig = {};
207
+ /** 모든 이벤트에 자동 첨부되는 글로벌 프로퍼티 (크로스 도메인 어트리뷰션 등) */
208
+ this.globalProperties = {};
209
+ this.recentEvents = /* @__PURE__ */ new Set();
210
+ this.dedupeTimers = /* @__PURE__ */ new Map();
211
+ }
212
+ /**
213
+ * Called after base initialization, override for platform-specific setup
214
+ */
215
+ onInitialized() {
216
+ }
217
+ /**
218
+ * Called before cleanup, override for platform-specific teardown
219
+ */
220
+ onCleanup() {
221
+ }
222
+ async initialize(config, context) {
223
+ this.pluginConfig = { ...this.getDefaultConfig(), ...config };
224
+ this.context = context;
225
+ this.adapter = this.createAdapter();
226
+ this.adapter.initialize(this.pluginConfig, context);
227
+ if (this.pluginConfig.trackNetworkRequests !== false) {
228
+ const patterns = this.getNetworkPatterns();
229
+ this.interceptor = new NetworkInterceptor(patterns, {
230
+ onRequest: (url, method, body) => {
231
+ this.handleNetworkRequest(url, method, body);
232
+ },
233
+ onResponse: (url, status, body) => {
234
+ this.handleNetworkResponse(url, status, body);
235
+ }
236
+ });
237
+ this.interceptor.start();
238
+ }
239
+ if (this.pluginConfig.trackPageViews !== false) {
240
+ this.setupNavigationTracking();
241
+ }
242
+ this.initCampaignParams();
243
+ this.onInitialized();
244
+ this.context.logger.info("Plugin initialized");
245
+ }
246
+ async cleanup() {
247
+ this.onCleanup();
248
+ if (this.interceptor) {
249
+ this.interceptor.stop();
250
+ this.interceptor = void 0;
251
+ }
252
+ if (this.navigationCleanup) {
253
+ this.navigationCleanup();
254
+ this.navigationCleanup = void 0;
255
+ }
256
+ if (this.adapter) {
257
+ this.adapter.cleanup();
258
+ this.adapter = void 0;
259
+ }
260
+ this.recentEvents.clear();
261
+ for (const timer of this.dedupeTimers.values()) {
262
+ clearTimeout(timer);
263
+ }
264
+ this.dedupeTimers.clear();
265
+ this.context?.logger.info("Plugin cleaned up");
266
+ this.context = void 0;
267
+ }
268
+ /**
269
+ * Track an e-commerce event with deduplication
270
+ */
271
+ trackEvent(event, properties) {
272
+ if (!this.context) return;
273
+ const identifier = properties.product_id || properties.order_id || properties.url || (Array.isArray(properties.items) ? `items:${properties.items.length}` : "");
274
+ const dedupeKey = `${event}:${identifier}`;
275
+ const dedupeWindow = this.pluginConfig.deduplicationWindow || DEFAULT_DEDUPLICATION_WINDOW;
276
+ if (this.recentEvents.has(dedupeKey)) {
277
+ this.context.logger.debug(`Deduplicating event: ${event}`);
278
+ return;
279
+ }
280
+ this.recentEvents.add(dedupeKey);
281
+ const timer = setTimeout(() => {
282
+ this.recentEvents.delete(dedupeKey);
283
+ this.dedupeTimers.delete(dedupeKey);
284
+ }, dedupeWindow);
285
+ this.dedupeTimers.set(dedupeKey, timer);
286
+ const meta = this.adapter?.getPlatformMeta() || {};
287
+ const enrichedProperties = {
288
+ ...this.globalProperties,
289
+ ...properties,
290
+ platform: meta
291
+ };
292
+ this.context.track(event, enrichedProperties);
293
+ }
294
+ /**
295
+ * Handle intercepted network request — override in subclass for specific behavior
296
+ */
297
+ handleNetworkRequest(_url, _method, _body) {
298
+ }
299
+ /**
300
+ * Handle intercepted network response — override in subclass for specific behavior
301
+ */
302
+ handleNetworkResponse(_url, _status, _body) {
303
+ }
304
+ /**
305
+ * Handle page navigation
306
+ */
307
+ handlePageNavigation(url) {
308
+ if (!this.adapter || !this.context) return;
309
+ const pageType = this.adapter.detectPageType();
310
+ this.trackEvent("page_viewed" /* PAGE_VIEWED */, {
311
+ url,
312
+ page_type: pageType,
313
+ title: typeof document !== "undefined" ? document.title : void 0
314
+ });
315
+ }
316
+ setupNavigationTracking() {
317
+ if (typeof window === "undefined") return;
318
+ const handleNavigation = () => {
319
+ this.handlePageNavigation(window.location.href);
320
+ };
321
+ const originalPushState = history.pushState;
322
+ const originalReplaceState = history.replaceState;
323
+ history.pushState = function(...args) {
324
+ const result = originalPushState.apply(this, args);
325
+ try {
326
+ handleNavigation();
327
+ } catch (_) {
328
+ }
329
+ return result;
330
+ };
331
+ history.replaceState = function(...args) {
332
+ const result = originalReplaceState.apply(this, args);
333
+ try {
334
+ handleNavigation();
335
+ } catch (_) {
336
+ }
337
+ return result;
338
+ };
339
+ window.addEventListener("popstate", handleNavigation);
340
+ this.navigationCleanup = () => {
341
+ history.pushState = originalPushState;
342
+ history.replaceState = originalReplaceState;
343
+ window.removeEventListener("popstate", handleNavigation);
344
+ };
345
+ handleNavigation();
346
+ }
347
+ /**
348
+ * URL의 캠페인 파라미터(UTM + 광고 ID)를 수집하여 globalProperties에 설정.
349
+ * sessionStorage를 사용하여 MPA 페이지 전환 간에도 유지.
350
+ */
351
+ initCampaignParams() {
352
+ if (typeof window === "undefined") return;
353
+ const params = new URLSearchParams(window.location.search);
354
+ let hasUrlCampaign = false;
355
+ for (const param of CAMPAIGN_PARAMS) {
356
+ if (params.get(param)) {
357
+ hasUrlCampaign = true;
358
+ break;
359
+ }
360
+ }
361
+ if (hasUrlCampaign) {
362
+ for (const param of CAMPAIGN_PARAMS) {
363
+ try {
364
+ sessionStorage.removeItem(CAMPAIGN_STORAGE_PREFIX + param);
365
+ } catch {
366
+ }
367
+ }
368
+ }
369
+ for (const param of CAMPAIGN_PARAMS) {
370
+ const value = params.get(param);
371
+ if (value) {
372
+ try {
373
+ sessionStorage.setItem(CAMPAIGN_STORAGE_PREFIX + param, value);
374
+ } catch {
375
+ }
376
+ }
377
+ }
378
+ for (const param of CAMPAIGN_PARAMS) {
379
+ try {
380
+ const stored = sessionStorage.getItem(CAMPAIGN_STORAGE_PREFIX + param);
381
+ if (stored) {
382
+ this.globalProperties[param] = stored;
383
+ }
384
+ } catch {
385
+ }
386
+ }
387
+ }
388
+ getDefaultConfig() {
389
+ return {
390
+ trackPageViews: true,
391
+ trackNetworkRequests: true,
392
+ trackDomInteractions: true,
393
+ deduplicationWindow: DEFAULT_DEDUPLICATION_WINDOW,
394
+ debug: false
395
+ };
396
+ }
397
+ };
398
+
399
+ export { BaseEcommercePlugin, EcommerceEvent, NetworkInterceptor, PageType };
400
+ //# sourceMappingURL=index.mjs.map
401
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/NetworkInterceptor.ts","../src/types.ts","../src/BaseEcommercePlugin.ts"],"names":["EcommerceEvent","PageType"],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * Network interceptor for capturing fetch/XHR requests\n * Patches window.fetch and XMLHttpRequest to intercept matching API calls\n */\n\nimport type { NetworkPattern, NetworkInterceptCallback } from './types';\n\nexport class NetworkInterceptor {\n private patterns: NetworkPattern[] = [];\n private callback: NetworkInterceptCallback;\n private originalFetch?: typeof window.fetch;\n private originalXHROpen?: typeof XMLHttpRequest.prototype.open;\n private originalXHRSend?: typeof XMLHttpRequest.prototype.send;\n private isActive = false;\n\n constructor(patterns: NetworkPattern[], callback: NetworkInterceptCallback) {\n this.patterns = patterns;\n this.callback = callback;\n }\n\n /**\n * Start intercepting network requests\n */\n start(): void {\n if (this.isActive) return;\n if (typeof window === 'undefined') return;\n\n this.patchFetch();\n this.patchXHR();\n this.isActive = true;\n }\n\n /**\n * Stop intercepting and restore original functions\n */\n stop(): void {\n if (!this.isActive) return;\n\n this.restoreFetch();\n this.restoreXHR();\n this.isActive = false;\n }\n\n /**\n * Check if a URL matches any registered pattern\n */\n matchUrl(url: string, method?: string): NetworkPattern | undefined {\n return this.patterns.find((p) => {\n const urlMatch =\n p.pattern instanceof RegExp ? p.pattern.test(url) : url.includes(p.pattern);\n const methodMatch = !p.method || p.method === method?.toUpperCase();\n return urlMatch && methodMatch;\n });\n }\n\n private patchFetch(): void {\n if (typeof window.fetch !== 'function') return;\n\n this.originalFetch = window.fetch;\n const self = this;\n\n window.fetch = function (input: RequestInfo | URL, init?: RequestInit): Promise<Response> {\n const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url;\n const method = init?.method || 'GET';\n const pattern = self.matchUrl(url, method);\n\n if (pattern && self.callback.onRequest) {\n try {\n let body: any;\n try {\n if (init?.body) {\n body =\n typeof init.body === 'string'\n ? init.body\n : init.body instanceof FormData\n ? Object.fromEntries((init.body as any).entries())\n : init.body;\n }\n } catch {\n // Ignore body parsing errors\n }\n self.callback.onRequest(url, method, body);\n } catch {\n // Never let interceptor errors break application fetch calls\n }\n }\n\n return self.originalFetch!.call(window, input, init).then((response) => {\n if (pattern && self.callback.onResponse) {\n try {\n // Content-Type 체크: JSON 응답만 파싱 (대용량 바이너리 응답 OOM 방지)\n const contentType = response.headers.get('content-type') || '';\n if (!contentType.includes('application/json') && !contentType.includes('text/')) {\n try {\n self.callback.onResponse!(url, response.status, undefined);\n } catch {\n // Ignore onResponse callback errors\n }\n } else {\n const cloned = response.clone();\n // text() 먼저 호출 후 JSON.parse 시도 (clone body는 1회만 소비 가능)\n cloned\n .text()\n .then((text) => {\n try {\n const json = JSON.parse(text);\n self.callback.onResponse!(url, response.status, json);\n } catch {\n try {\n self.callback.onResponse!(url, response.status, text);\n } catch {\n // Ignore onResponse callback errors\n }\n }\n })\n .catch(() => {\n try {\n self.callback.onResponse!(url, response.status, undefined);\n } catch {\n // Ignore onResponse callback errors\n }\n });\n }\n } catch {\n // Ignore clone/response handling errors to avoid breaking the app\n }\n }\n return response;\n });\n };\n }\n\n private patchXHR(): void {\n if (typeof XMLHttpRequest === 'undefined') return;\n\n this.originalXHROpen = XMLHttpRequest.prototype.open;\n this.originalXHRSend = XMLHttpRequest.prototype.send;\n const self = this;\n\n XMLHttpRequest.prototype.open = function (\n method: string,\n url: string | URL,\n ...args: any[]\n ) {\n (this as any).__vircle_url = typeof url === 'string' ? url : url.toString();\n (this as any).__vircle_method = method;\n return self.originalXHROpen!.apply(this, [method, url, ...args] as any);\n };\n\n XMLHttpRequest.prototype.send = function (body?: Document | XMLHttpRequestBodyInit | null) {\n const url = (this as any).__vircle_url as string;\n const method = (this as any).__vircle_method as string;\n const pattern = url ? self.matchUrl(url, method) : undefined;\n\n if (pattern && self.callback.onRequest) {\n try {\n let parsedBody: any;\n try {\n if (typeof body === 'string') {\n parsedBody = body;\n } else if (body instanceof FormData) {\n parsedBody = Object.fromEntries((body as any).entries());\n }\n } catch {\n // Ignore body parsing errors\n }\n self.callback.onRequest(url, method, parsedBody);\n } catch {\n // Never let interceptor errors break application XHR calls\n }\n }\n\n if (pattern && self.callback.onResponse) {\n this.addEventListener('load', function () {\n try {\n // Content-Type 체크: JSON/text 응답만 파싱\n const contentType = this.getResponseHeader('content-type') || '';\n if (!contentType.includes('application/json') && !contentType.includes('text/')) {\n self.callback.onResponse!(url, this.status, undefined);\n return;\n }\n const responseBody =\n this.responseType === '' || this.responseType === 'text'\n ? JSON.parse(this.responseText)\n : this.response;\n self.callback.onResponse!(url, this.status, responseBody);\n } catch {\n try {\n self.callback.onResponse!(url, this.status, this.responseText);\n } catch {\n // Ignore onResponse callback errors\n }\n }\n });\n }\n\n return self.originalXHRSend!.call(this, body);\n };\n }\n\n private restoreFetch(): void {\n if (this.originalFetch && typeof window !== 'undefined') {\n window.fetch = this.originalFetch;\n this.originalFetch = undefined;\n }\n }\n\n private restoreXHR(): void {\n if (typeof XMLHttpRequest === 'undefined') return;\n\n if (this.originalXHROpen) {\n XMLHttpRequest.prototype.open = this.originalXHROpen;\n this.originalXHROpen = undefined;\n }\n if (this.originalXHRSend) {\n XMLHttpRequest.prototype.send = this.originalXHRSend;\n this.originalXHRSend = undefined;\n }\n }\n}\n","/**\n * E-commerce plugin core types\n */\n\nimport type { PluginContext } from '@vircle/sdk-core-ts';\n\n/**\n * Standard e-commerce event names\n */\nexport enum EcommerceEvent {\n PAGE_VIEWED = 'page_viewed',\n PRODUCT_VIEWED = 'product_viewed',\n PRODUCT_ADDED_TO_CART = 'product_added_to_cart',\n PRODUCT_REMOVED_FROM_CART = 'product_removed_from_cart',\n CART_VIEWED = 'cart_viewed',\n CART_UPDATED = 'cart_updated',\n PRODUCT_ADDED_TO_WISHLIST = 'product_added_to_wishlist',\n CHECKOUT_STARTED = 'checkout_started',\n EXTERNAL_PAYMENT_INITIATED = 'external_payment_initiated',\n ORDER_COMPLETED = 'order_completed',\n}\n\n/**\n * Product sub-object schema\n */\nexport interface Product {\n product_id: string;\n name: string;\n price: number;\n sku?: string;\n category?: string;\n brand?: string;\n variant?: string;\n quantity?: number;\n currency?: string;\n image_url?: string;\n url?: string;\n position?: number;\n}\n\n/**\n * Cart item sub-object schema\n */\nexport interface CartItem extends Product {\n quantity: number;\n item_total?: number;\n}\n\n/**\n * Order item sub-object schema\n */\nexport interface OrderItem extends Product {\n quantity: number;\n item_total: number;\n discount?: number;\n}\n\n/**\n * E-commerce plugin configuration\n */\nexport interface EcommercePluginConfig {\n /** Enable automatic page view tracking */\n trackPageViews?: boolean;\n /** Enable network request interception */\n trackNetworkRequests?: boolean;\n /** Enable DOM-based tracking */\n trackDomInteractions?: boolean;\n /** Custom URL patterns to intercept */\n urlPatterns?: NetworkPattern[];\n /** Event deduplication window in ms */\n deduplicationWindow?: number;\n /** Debug mode */\n debug?: boolean;\n}\n\n/**\n * Network URL pattern for interception\n */\nexport interface NetworkPattern {\n /** URL pattern (string match or regex) */\n pattern: string | RegExp;\n /** HTTP method filter */\n method?: 'GET' | 'POST' | 'PUT' | 'DELETE';\n /** Event to emit when matched */\n event: EcommerceEvent;\n}\n\n/**\n * Platform adapter interface — contract for platform-specific implementations\n */\nexport interface PlatformAdapter {\n /** Platform name */\n readonly name: string;\n\n /** Initialize adapter */\n initialize(config: EcommercePluginConfig, context: PluginContext): void;\n\n /** Cleanup adapter resources */\n cleanup(): void;\n\n /** Detect current page type */\n detectPageType(): PageType;\n\n /** Extract product data from current page */\n extractProduct(): Product | null;\n\n /** Extract cart items from current page */\n extractCartItems(): CartItem[];\n\n /** Extract order data from current page */\n extractOrder(): OrderData | null;\n\n /** Get platform-specific metadata */\n getPlatformMeta(): Record<string, any>;\n}\n\n/**\n * Page types for e-commerce sites\n */\nexport enum PageType {\n HOME = 'home',\n PRODUCT_LIST = 'product_list',\n PRODUCT_DETAIL = 'product_detail',\n CART = 'cart',\n CHECKOUT = 'checkout',\n ORDER_COMPLETE = 'order_complete',\n SEARCH_RESULTS = 'search_results',\n MY_PAGE = 'my_page',\n OTHER = 'other',\n}\n\n/**\n * Order data structure\n */\nexport interface OrderData {\n order_id: string;\n total: number;\n subtotal?: number;\n tax?: number;\n shipping?: number;\n discount?: number;\n currency?: string;\n payment_method?: string;\n items: OrderItem[];\n}\n\n/**\n * Network intercept callback\n */\nexport interface NetworkInterceptCallback {\n onRequest?(url: string, method: string, body?: any): void;\n onResponse?(url: string, status: number, body?: any): void;\n}\n","/**\n * Base e-commerce plugin that platform-specific plugins extend\n */\n\nimport type { VirclePlugin, PluginContext, PluginHooks } from '@vircle/sdk-core-ts';\nimport { NetworkInterceptor } from './NetworkInterceptor';\nimport { EcommerceEvent } from './types';\nimport type {\n EcommercePluginConfig,\n PlatformAdapter,\n NetworkPattern,\n} from './types';\n\nconst DEFAULT_DEDUPLICATION_WINDOW = 2000; // 2 seconds\n\nconst CAMPAIGN_PARAMS = [\n 'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'gclid', 'fbclid', 'msclkid', 'ttclid',\n];\nconst CAMPAIGN_STORAGE_PREFIX = '__vircle_campaign_';\n\nexport abstract class BaseEcommercePlugin implements VirclePlugin {\n abstract readonly name: string;\n abstract readonly version: string;\n abstract readonly description: string;\n\n protected context?: PluginContext;\n protected pluginConfig: EcommercePluginConfig = {};\n protected adapter?: PlatformAdapter;\n protected interceptor?: NetworkInterceptor;\n /** 모든 이벤트에 자동 첨부되는 글로벌 프로퍼티 (크로스 도메인 어트리뷰션 등) */\n protected globalProperties: Record<string, unknown> = {};\n private recentEvents = new Set<string>();\n private dedupeTimers = new Map<string, ReturnType<typeof setTimeout>>();\n private navigationCleanup?: () => void;\n\n hooks?: PluginHooks;\n\n /**\n * Get network patterns for this platform\n */\n protected abstract getNetworkPatterns(): NetworkPattern[];\n\n /**\n * Create the platform adapter\n */\n protected abstract createAdapter(): PlatformAdapter;\n\n /**\n * Called after base initialization, override for platform-specific setup\n */\n protected onInitialized(): void {\n // Override in subclass\n }\n\n /**\n * Called before cleanup, override for platform-specific teardown\n */\n protected onCleanup(): void {\n // Override in subclass\n }\n\n async initialize(config: EcommercePluginConfig, context: PluginContext): Promise<void> {\n this.pluginConfig = { ...this.getDefaultConfig(), ...config };\n this.context = context;\n\n // Create platform adapter\n this.adapter = this.createAdapter();\n this.adapter.initialize(this.pluginConfig, context);\n\n // Setup network interceptor\n if (this.pluginConfig.trackNetworkRequests !== false) {\n const patterns = this.getNetworkPatterns();\n this.interceptor = new NetworkInterceptor(patterns, {\n onRequest: (url, method, body) => {\n this.handleNetworkRequest(url, method, body);\n },\n onResponse: (url, status, body) => {\n this.handleNetworkResponse(url, status, body);\n },\n });\n this.interceptor.start();\n }\n\n // Setup navigation tracking\n if (this.pluginConfig.trackPageViews !== false) {\n this.setupNavigationTracking();\n }\n\n this.initCampaignParams();\n this.onInitialized();\n\n this.context.logger.info('Plugin initialized');\n }\n\n async cleanup(): Promise<void> {\n this.onCleanup();\n\n if (this.interceptor) {\n this.interceptor.stop();\n this.interceptor = undefined;\n }\n\n if (this.navigationCleanup) {\n this.navigationCleanup();\n this.navigationCleanup = undefined;\n }\n\n if (this.adapter) {\n this.adapter.cleanup();\n this.adapter = undefined;\n }\n\n this.recentEvents.clear();\n for (const timer of this.dedupeTimers.values()) {\n clearTimeout(timer);\n }\n this.dedupeTimers.clear();\n this.context?.logger.info('Plugin cleaned up');\n this.context = undefined;\n }\n\n /**\n * Track an e-commerce event with deduplication\n */\n protected trackEvent(event: EcommerceEvent, properties: Record<string, any>): void {\n if (!this.context) return;\n\n // Deduplication: skip if same event fired recently (key by event + primary identifier)\n const identifier = properties.product_id\n || properties.order_id\n || properties.url\n || (Array.isArray(properties.items) ? `items:${properties.items.length}` : '');\n const dedupeKey = `${event}:${identifier}`;\n const dedupeWindow = this.pluginConfig.deduplicationWindow || DEFAULT_DEDUPLICATION_WINDOW;\n\n if (this.recentEvents.has(dedupeKey)) {\n this.context.logger.debug(`Deduplicating event: ${event}`);\n return;\n }\n\n this.recentEvents.add(dedupeKey);\n // 자동 만료: deduplicationWindow 후 자동 삭제\n const timer = setTimeout(() => {\n this.recentEvents.delete(dedupeKey);\n this.dedupeTimers.delete(dedupeKey);\n }, dedupeWindow);\n this.dedupeTimers.set(dedupeKey, timer);\n\n // Add global properties and platform metadata\n const meta = this.adapter?.getPlatformMeta() || {};\n const enrichedProperties = {\n ...this.globalProperties,\n ...properties,\n platform: meta,\n };\n\n this.context.track(event, enrichedProperties);\n }\n\n /**\n * Handle intercepted network request — override in subclass for specific behavior\n */\n protected handleNetworkRequest(_url: string, _method: string, _body?: any): void {\n // Default: no-op, subclasses implement specific logic\n }\n\n /**\n * Handle intercepted network response — override in subclass for specific behavior\n */\n protected handleNetworkResponse(_url: string, _status: number, _body?: any): void {\n // Default: no-op, subclasses implement specific logic\n }\n\n /**\n * Handle page navigation\n */\n protected handlePageNavigation(url: string): void {\n if (!this.adapter || !this.context) return;\n\n const pageType = this.adapter.detectPageType();\n\n this.trackEvent(EcommerceEvent.PAGE_VIEWED, {\n url,\n page_type: pageType,\n title: typeof document !== 'undefined' ? document.title : undefined,\n });\n }\n\n private setupNavigationTracking(): void {\n if (typeof window === 'undefined') return;\n\n const handleNavigation = () => {\n this.handlePageNavigation(window.location.href);\n };\n\n // Listen for History API changes\n const originalPushState = history.pushState;\n const originalReplaceState = history.replaceState;\n\n history.pushState = function (...args) {\n const result = originalPushState.apply(this, args);\n try { handleNavigation(); } catch (_) { /* SDK 오류가 호스트 앱 라우팅에 영향 주지 않도록 */ }\n return result;\n };\n\n history.replaceState = function (...args) {\n const result = originalReplaceState.apply(this, args);\n try { handleNavigation(); } catch (_) { /* SDK 오류가 호스트 앱 라우팅에 영향 주지 않도록 */ }\n return result;\n };\n\n window.addEventListener('popstate', handleNavigation);\n\n this.navigationCleanup = () => {\n history.pushState = originalPushState;\n history.replaceState = originalReplaceState;\n window.removeEventListener('popstate', handleNavigation);\n };\n\n // Track initial page view\n handleNavigation();\n }\n\n /**\n * URL의 캠페인 파라미터(UTM + 광고 ID)를 수집하여 globalProperties에 설정.\n * sessionStorage를 사용하여 MPA 페이지 전환 간에도 유지.\n */\n private initCampaignParams(): void {\n if (typeof window === 'undefined') return;\n\n const params = new URLSearchParams(window.location.search);\n\n // URL에 캠페인 파라미터가 하나라도 있으면 새 캠페인 진입 — 기존 값 클리어 후 새로 저장\n let hasUrlCampaign = false;\n for (const param of CAMPAIGN_PARAMS) {\n if (params.get(param)) {\n hasUrlCampaign = true;\n break;\n }\n }\n if (hasUrlCampaign) {\n for (const param of CAMPAIGN_PARAMS) {\n try { sessionStorage.removeItem(CAMPAIGN_STORAGE_PREFIX + param); } catch {}\n }\n }\n\n // URL에 캠페인 파라미터가 있으면 sessionStorage에 저장\n for (const param of CAMPAIGN_PARAMS) {\n const value = params.get(param);\n if (value) {\n try {\n sessionStorage.setItem(CAMPAIGN_STORAGE_PREFIX + param, value);\n } catch {}\n }\n }\n\n // sessionStorage에서 복원하여 globalProperties에 설정\n for (const param of CAMPAIGN_PARAMS) {\n try {\n const stored = sessionStorage.getItem(CAMPAIGN_STORAGE_PREFIX + param);\n if (stored) {\n this.globalProperties[param] = stored;\n }\n } catch {}\n }\n }\n\n private getDefaultConfig(): EcommercePluginConfig {\n return {\n trackPageViews: true,\n trackNetworkRequests: true,\n trackDomInteractions: true,\n deduplicationWindow: DEFAULT_DEDUPLICATION_WINDOW,\n debug: false,\n };\n }\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@vircle/plugin-ecommerce-core",
3
+ "version": "0.1.0",
4
+ "description": "Vircle SDK E-commerce Plugin Core - Base classes and utilities for commerce tracking plugins",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "keywords": [
12
+ "vircle",
13
+ "analytics",
14
+ "ecommerce",
15
+ "plugin",
16
+ "tracking"
17
+ ],
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/mass-adoption/vircle-sdk.git",
21
+ "directory": "packages/plugin-ecommerce-core"
22
+ },
23
+ "author": "Vircle <it@vircle.co.kr> (https://vircle.co.kr)",
24
+ "license": "MIT",
25
+ "publishConfig": {
26
+ "access": "public",
27
+ "registry": "https://registry.npmjs.org/"
28
+ },
29
+ "dependencies": {
30
+ "@vircle/sdk-core-ts": "1.1.1"
31
+ },
32
+ "devDependencies": {
33
+ "@types/node": "^20.0.0",
34
+ "jsdom": "^26.1.0",
35
+ "tsup": "^8.0.0",
36
+ "typescript": "^5.0.0",
37
+ "vitest": "^1.0.0"
38
+ },
39
+ "engines": {
40
+ "node": ">= 18.0.0"
41
+ },
42
+ "sideEffects": false,
43
+ "scripts": {
44
+ "build": "tsup",
45
+ "dev": "tsup --watch",
46
+ "test": "vitest --passWithNoTests",
47
+ "typecheck": "tsc --noEmit",
48
+ "clean": "rm -rf dist"
49
+ }
50
+ }