lytx 0.3.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.
Files changed (213) hide show
  1. package/.env.example +37 -0
  2. package/README.md +486 -0
  3. package/alchemy.run.ts +155 -0
  4. package/cli/bootstrap-admin.ts +284 -0
  5. package/cli/deploy-staging.ts +692 -0
  6. package/cli/import-events.ts +628 -0
  7. package/cli/import-sites.ts +518 -0
  8. package/cli/index.ts +609 -0
  9. package/cli/init-db.ts +269 -0
  10. package/cli/migrate-to-durable-objects.ts +564 -0
  11. package/cli/migration-worker.ts +300 -0
  12. package/cli/performance-test.ts +588 -0
  13. package/cli/pg/client.ts +4 -0
  14. package/cli/pg/new-site.ts +153 -0
  15. package/cli/rollback-durable-objects.ts +622 -0
  16. package/cli/seed-data.ts +459 -0
  17. package/cli/setup.js +18 -0
  18. package/cli/setup.ts +463 -0
  19. package/cli/validate-migration.ts +200 -0
  20. package/cli/wrangler-migration.jsonc +28 -0
  21. package/db/adapter.ts +166 -0
  22. package/db/analytics_engine/client.ts +0 -0
  23. package/db/analytics_engine/sites.ts +0 -0
  24. package/db/client.ts +16 -0
  25. package/db/d1/client.ts +8 -0
  26. package/db/d1/drizzle.config.ts +35 -0
  27. package/db/d1/migrations/0000_true_maelstrom.sql +165 -0
  28. package/db/d1/migrations/0001_wonderful_bloodaxe.sql +12 -0
  29. package/db/d1/migrations/0002_late_frightful_four.sql +1 -0
  30. package/db/d1/migrations/0003_cuddly_obadiah_stane.sql +16 -0
  31. package/db/d1/migrations/0004_mute_stardust.sql +1 -0
  32. package/db/d1/migrations/0005_awesome_silvermane.sql +3 -0
  33. package/db/d1/migrations/0006_volatile_shriek.sql +2 -0
  34. package/db/d1/migrations/0007_superb_lila_cheney.sql +1 -0
  35. package/db/d1/migrations/0008_bitter_longshot.sql +17 -0
  36. package/db/d1/migrations/0009_wonderful_madame_masque.sql +28 -0
  37. package/db/d1/migrations/meta/0000_snapshot.json +1112 -0
  38. package/db/d1/migrations/meta/0001_snapshot.json +1187 -0
  39. package/db/d1/migrations/meta/0002_snapshot.json +1194 -0
  40. package/db/d1/migrations/meta/0003_snapshot.json +1296 -0
  41. package/db/d1/migrations/meta/0004_snapshot.json +1303 -0
  42. package/db/d1/migrations/meta/0005_snapshot.json +1325 -0
  43. package/db/d1/migrations/meta/0006_snapshot.json +1339 -0
  44. package/db/d1/migrations/meta/0007_snapshot.json +1347 -0
  45. package/db/d1/migrations/meta/0008_snapshot.json +1464 -0
  46. package/db/d1/migrations/meta/0009_snapshot.json +1648 -0
  47. package/db/d1/migrations/meta/_journal.json +76 -0
  48. package/db/d1/schema.ts +407 -0
  49. package/db/d1/sites.ts +374 -0
  50. package/db/d1/teamAiUsage.ts +101 -0
  51. package/db/d1/teams.ts +127 -0
  52. package/db/durable/drizzle.config.ts +8 -0
  53. package/db/durable/durableObjectClient.ts +480 -0
  54. package/db/durable/events.ts +100 -0
  55. package/db/durable/migrations/0000_fair_bucky.sql +38 -0
  56. package/db/durable/migrations/meta/0000_snapshot.json +278 -0
  57. package/db/durable/migrations/meta/_journal.json +13 -0
  58. package/db/durable/migrations/migrations.js +10 -0
  59. package/db/durable/schema.ts +5 -0
  60. package/db/durable/siteDurableObject.ts +1352 -0
  61. package/db/durable/types.ts +53 -0
  62. package/db/postgres/client.ts +13 -0
  63. package/db/postgres/drizzle.config.ts +12 -0
  64. package/db/postgres/migrations/0000_brainy_sprite.sql +116 -0
  65. package/db/postgres/migrations/meta/0000_snapshot.json +681 -0
  66. package/db/postgres/migrations/meta/_journal.json +13 -0
  67. package/db/postgres/schema.ts +145 -0
  68. package/db/postgres/sites.ts +118 -0
  69. package/db/tranformReports.ts +595 -0
  70. package/db/types.ts +55 -0
  71. package/endpoints/api_worker.tsx +1854 -0
  72. package/endpoints/site_do_worker.ts +11 -0
  73. package/index.d.ts +63 -0
  74. package/index.ts +83 -0
  75. package/lib/auth.ts +279 -0
  76. package/lib/geojson/world_countries.json +45307 -0
  77. package/lib/random_name.ts +41 -0
  78. package/lib/sendMail.ts +252 -0
  79. package/package.json +142 -0
  80. package/public/favicon.ico +0 -0
  81. package/public/images/android-chrome-192x192.png +0 -0
  82. package/public/images/android-chrome-512x512.png +0 -0
  83. package/public/images/apple-touch-icon.png +0 -0
  84. package/public/images/favicon-16x16.png +0 -0
  85. package/public/images/favicon-32x32.png +0 -0
  86. package/public/images/lytx_dark_dashboard.png +0 -0
  87. package/public/images/lytx_light_dashboard.png +0 -0
  88. package/public/images/safari-pinned-tab.svg +4 -0
  89. package/public/logo.png +0 -0
  90. package/public/site.webmanifest +26 -0
  91. package/public/sw.js +107 -0
  92. package/src/Document.tsx +86 -0
  93. package/src/api/ai_api.ts +1156 -0
  94. package/src/api/authMiddleware.ts +45 -0
  95. package/src/api/auth_api.ts +465 -0
  96. package/src/api/event_labels_api.ts +193 -0
  97. package/src/api/events_api.ts +210 -0
  98. package/src/api/queueWorker.ts +303 -0
  99. package/src/api/reports_api.ts +278 -0
  100. package/src/api/seed_api.ts +288 -0
  101. package/src/api/sites_api.ts +904 -0
  102. package/src/api/tag_api.ts +458 -0
  103. package/src/api/tag_api_v2.ts +289 -0
  104. package/src/api/team_api.ts +456 -0
  105. package/src/app/Dashboard.tsx +1339 -0
  106. package/src/app/Events.tsx +974 -0
  107. package/src/app/Explore.tsx +312 -0
  108. package/src/app/Layout.tsx +58 -0
  109. package/src/app/Settings.tsx +1302 -0
  110. package/src/app/components/DashboardCard.tsx +118 -0
  111. package/src/app/components/EditableCell.tsx +123 -0
  112. package/src/app/components/EventForm.tsx +93 -0
  113. package/src/app/components/MarketingFooter.tsx +49 -0
  114. package/src/app/components/MarketingNav.tsx +150 -0
  115. package/src/app/components/Nav.tsx +755 -0
  116. package/src/app/components/NewSiteSetup.tsx +298 -0
  117. package/src/app/components/SQLEditor.tsx +740 -0
  118. package/src/app/components/SiteSelector.tsx +126 -0
  119. package/src/app/components/SiteTag.tsx +42 -0
  120. package/src/app/components/SiteTagInstallCard.tsx +241 -0
  121. package/src/app/components/WorldMapCard.tsx +337 -0
  122. package/src/app/components/charts/ChartComponents.tsx +1481 -0
  123. package/src/app/components/charts/EventFunnel.tsx +45 -0
  124. package/src/app/components/charts/EventSummary.tsx +194 -0
  125. package/src/app/components/charts/SankeyFlows.tsx +72 -0
  126. package/src/app/components/marketing/CheckIcon.tsx +16 -0
  127. package/src/app/components/marketing/MarketingLayout.tsx +23 -0
  128. package/src/app/components/marketing/SectionHeading.tsx +35 -0
  129. package/src/app/components/reports/AskAiWorkspace.tsx +371 -0
  130. package/src/app/components/reports/CreateReportStarter.tsx +74 -0
  131. package/src/app/components/reports/DashboardRouteFiltersContext.tsx +14 -0
  132. package/src/app/components/reports/DashboardToolbar.tsx +154 -0
  133. package/src/app/components/reports/DashboardWorkspaceLayout.tsx +63 -0
  134. package/src/app/components/reports/DashboardWorkspaceShell.tsx +118 -0
  135. package/src/app/components/reports/ReportBuilderWorkspace.tsx +76 -0
  136. package/src/app/components/reports/custom/CustomReportBuilderPage.tsx +1667 -0
  137. package/src/app/components/reports/custom/ReportWidgetChart.tsx +297 -0
  138. package/src/app/components/reports/custom/buildWidgetSql.ts +151 -0
  139. package/src/app/components/reports/custom/chartPalettes.ts +18 -0
  140. package/src/app/components/reports/custom/types.ts +50 -0
  141. package/src/app/components/reports/reportBuilderMenuItems.ts +17 -0
  142. package/src/app/components/reports/useDashboardToolbarControls.tsx +235 -0
  143. package/src/app/components/ui/AlertBanner.tsx +101 -0
  144. package/src/app/components/ui/Button.tsx +55 -0
  145. package/src/app/components/ui/Card.tsx +80 -0
  146. package/src/app/components/ui/Input.tsx +72 -0
  147. package/src/app/components/ui/Link.tsx +23 -0
  148. package/src/app/components/ui/ReportBuilderMenu.tsx +246 -0
  149. package/src/app/components/ui/ThemeToggle.tsx +54 -0
  150. package/src/app/constants.ts +6 -0
  151. package/src/app/headers.ts +33 -0
  152. package/src/app/providers/AuthProvider.tsx +189 -0
  153. package/src/app/providers/ClientProviders.tsx +18 -0
  154. package/src/app/providers/QueryProvider.tsx +23 -0
  155. package/src/app/providers/ThemeProvider.tsx +88 -0
  156. package/src/app/utils/chartThemes.ts +146 -0
  157. package/src/app/utils/keybinds.ts +96 -0
  158. package/src/app/utils/media.tsx +24 -0
  159. package/src/client.tsx +114 -0
  160. package/src/config/createLytxAppConfig.ts +252 -0
  161. package/src/config/resourceNames.ts +88 -0
  162. package/src/db/index.ts +67 -0
  163. package/src/index.css +285 -0
  164. package/src/lib/featureFlags.ts +69 -0
  165. package/src/pages/GetStarted.tsx +290 -0
  166. package/src/pages/Home.tsx +268 -0
  167. package/src/pages/Login.tsx +283 -0
  168. package/src/pages/PrivacyPolicy.tsx +120 -0
  169. package/src/pages/Signup.tsx +267 -0
  170. package/src/pages/TermsOfService.tsx +126 -0
  171. package/src/pages/VerifyEmail.tsx +56 -0
  172. package/src/session/durableObject.ts +7 -0
  173. package/src/session/siteSchema.ts +86 -0
  174. package/src/session/types.ts +36 -0
  175. package/src/templates/README.md +80 -0
  176. package/src/templates/cleanFunctions.js +44 -0
  177. package/src/templates/embedFunctions.js +52 -0
  178. package/src/templates/lytx-shared.ts +662 -0
  179. package/src/templates/lytxpixel-core.ts +144 -0
  180. package/src/templates/lytxpixel.ts +267 -0
  181. package/src/templates/lytxpixelBrowser.js +634 -0
  182. package/src/templates/lytxpixelBrowser.mjs +634 -0
  183. package/src/templates/parseData.js +12 -0
  184. package/src/templates/script.ts +31 -0
  185. package/src/templates/template.tsx +50 -0
  186. package/src/templates/test.js +3 -0
  187. package/src/templates/trackWebEvents.ts +177 -0
  188. package/src/templates/vendors/clickcease.ts +8 -0
  189. package/src/templates/vendors/google.ts +174 -0
  190. package/src/templates/vendors/linkedin.ts +23 -0
  191. package/src/templates/vendors/meta.ts +56 -0
  192. package/src/templates/vendors/quantcast.ts +22 -0
  193. package/src/templates/vendors/simplfi.ts +7 -0
  194. package/src/types/app-context.ts +16 -0
  195. package/src/utilities/dashboardParams.ts +188 -0
  196. package/src/utilities/dashboardQueries.ts +537 -0
  197. package/src/utilities/dashboardTransforms.ts +167 -0
  198. package/src/utilities/dataValidation.ts +414 -0
  199. package/src/utilities/detector.ts +73 -0
  200. package/src/utilities/encrypt.ts +103 -0
  201. package/src/utilities/index.ts +13 -0
  202. package/src/utilities/parser.ts +117 -0
  203. package/src/utilities/performanceMonitoring.ts +570 -0
  204. package/src/utilities/route_interuptors.ts +24 -0
  205. package/src/worker.tsx +675 -0
  206. package/tsconfig.json +78 -0
  207. package/types/env.d.ts +16 -0
  208. package/types/rw.d.ts +7 -0
  209. package/types/shims.d.ts +53 -0
  210. package/types/vite.d.ts +19 -0
  211. package/vite/vite-plugin-pixel-bundle.ts +126 -0
  212. package/vite.config.ts +53 -0
  213. package/worker-configuration.d.ts +8401 -0
@@ -0,0 +1,662 @@
1
+ /**
2
+ * Lytx Script - Shared Module
3
+ *
4
+ * Contains types, utilities, and core functions shared between
5
+ * the full (tag_manager) and core versions of the script.
6
+ */
7
+ import type { Platform } from "./trackWebEvents";
8
+ import { trackEvents } from "./trackWebEvents";
9
+
10
+ // ============================================================================
11
+ // Types
12
+ // ============================================================================
13
+
14
+ export interface LytxEvent {
15
+ lytxAccount?: string;
16
+ labels?: string;
17
+ }
18
+
19
+ export type SiteConfig = { site: string, tag: string, autocapture?: boolean };
20
+
21
+ export type rule =
22
+ | "starts with"
23
+ | "does not contain"
24
+ | "ends with"
25
+ | "equals"
26
+ | "contains"
27
+ | "click"
28
+ | "hover"
29
+ | "is visible"
30
+ | "submit"
31
+ | "swipe";
32
+
33
+ export type condition =
34
+ | "path"
35
+ | "dom element"
36
+ | "domain"
37
+ | "element"
38
+ | "form field filled";
39
+
40
+ export type eventName =
41
+ | "View Content"
42
+ | "Search"
43
+ | "Add To Wishlist"
44
+ | "Add To Cart"
45
+ | "Initiate Checkout"
46
+ | "Add Payment Info"
47
+ | "Purchase"
48
+ | "Lead"
49
+ | "Register"
50
+ | "Start Trial"
51
+ | "Subscribe"
52
+ | "Submit Application"
53
+ | "Custom"
54
+ | "Thank You Page"
55
+ | "Page Visit"
56
+ | "Submit/Complete";
57
+
58
+ export type dataPassback = {
59
+ element: string,
60
+ value: string
61
+ }
62
+
63
+ export type paramOptionsKey = "querySelectorAll" | "path" | "fullpath" | 'delay' | 'iframe';
64
+ export type paramaOptionsValue = number | string | boolean;
65
+ export type paramConfig = Record<paramOptionsKey, paramaOptionsValue>;
66
+
67
+ /** Core PageEvent - fields common to both versions */
68
+ export interface PageEventCore {
69
+ Notes: string;
70
+ condition: condition;
71
+ data_passback?: string;
72
+ event_name: eventName;
73
+ parameters: string;
74
+ paramConfig: string;
75
+ query_parameters?: string;
76
+ rules: rule;
77
+ personalization?: Array<userInteraction>;
78
+ }
79
+
80
+ /** Full PageEvent - includes third-party vendor fields */
81
+ export interface PageEvent extends PageEventCore {
82
+ QuantcastPixelId?: string;
83
+ QuantCastPixelLabel?: string;
84
+ SimplfiPixelid?: string;
85
+ googleanalytics?: string;
86
+ googleadsscript?: string;
87
+ googleadsconversion?: string;
88
+ metaEvent?: string;
89
+ linkedinEvent?: string;
90
+ clickCease?: "enabled" | "disabled";
91
+ customScript?: string;
92
+ }
93
+
94
+ export interface userOption {
95
+ category: string;
96
+ relatesTo: string;
97
+ }
98
+
99
+ export interface userInteraction {
100
+ rule: rule;
101
+ record: boolean;
102
+ options: userOption
103
+ }
104
+
105
+ // ============================================================================
106
+ // Global Window Declaration
107
+ // ============================================================================
108
+
109
+ declare global {
110
+ interface Window {
111
+ _lytxEvents: LytxEvent[] | PageEvent[];
112
+ dataLayer: Array<unknown>;
113
+ _qevents: { qacct: string, labels: string }[];
114
+ gtag_lytx: Function;
115
+ gtag?: Function;
116
+ fbq?: (method: 'track' | 'trackCustom' | 'init', event: string, customParams?: Record<string, unknown>) => void;
117
+ _fbq: Function;
118
+ lintrk?: Function;
119
+ _linkedin_data_partner_ids?: Array<any>
120
+ lytxApi: {
121
+ emit: Function;
122
+ event: Function;
123
+ rid: Function;
124
+ debugMode: boolean;
125
+ platform: Platform;
126
+ currentSiteConfig: SiteConfig;
127
+ trackCustomEvents: Function;
128
+ capture: (eventName: string, customData?: Record<string, string>) => void;
129
+ track_web_events: boolean;
130
+ /** Tracks manually captured events to avoid autocapture duplicates */
131
+ _manualCaptures: Set<string>;
132
+ }
133
+ lytxDataLayer: Array<{
134
+ site: string,
135
+ tag: string,
136
+ events: [],
137
+ tracked: Array<string | Record<string, string> | PageEvent>
138
+ rid: null | string;
139
+ }>
140
+ }
141
+ }
142
+
143
+ // ============================================================================
144
+ // DOM Utilities
145
+ // ============================================================================
146
+
147
+ export function createScriptElement(src: string, async: boolean, type?: string) {
148
+ const script = document.createElement("script");
149
+ script.src = src;
150
+ script.async = async;
151
+ let scriptPlacement = document.head.children[0];
152
+ if (!scriptPlacement) {
153
+ scriptPlacement = document.getElementsByTagName("script")[0];
154
+ }
155
+ if (type) {
156
+ script.type = type;
157
+ }
158
+ scriptPlacement.parentNode!.insertBefore(script, scriptPlacement);
159
+ }
160
+
161
+ export function decodeHtmlEntities(str: string) {
162
+ return str.replaceAll(/&#39;/g, "'")
163
+ .replaceAll(/&quot;/g, '"')
164
+ .replaceAll(/&gt;/g, '>')
165
+ .replaceAll(/&lt;/g, '<')
166
+ .replaceAll(/&amp;/g, '&');
167
+ }
168
+
169
+ export function customScript(script: string) {
170
+ if (window.lytxApi.debugMode) {
171
+ console.info('Custom script is : ', script);
172
+ }
173
+ try {
174
+ let parsedScript = decodeHtmlEntities(script);
175
+ const createCustomScript = document.createElement('script');
176
+ createCustomScript.innerHTML = parsedScript;
177
+ const scriptPlacement = document.getElementsByTagName("script")[0];
178
+ scriptPlacement.parentNode!.insertBefore(createCustomScript, scriptPlacement);
179
+ } catch (error) {
180
+ console.error(error);
181
+ }
182
+ }
183
+
184
+ // ============================================================================
185
+ // Query Parameter Utilities
186
+ // ============================================================================
187
+
188
+ export function splitQueryParams(
189
+ split: string[] | string,
190
+ queryParams: URLSearchParams,
191
+ skipSmallSplit = false
192
+ ) {
193
+ let keys: { key: string; val: string; }[] = [];
194
+
195
+ if (!skipSmallSplit && typeof (split) != 'string') {
196
+ keys = split.map((values) => {
197
+ let smallSplit = values.split('=');
198
+ return { key: smallSplit[0], val: smallSplit[1] }
199
+ })
200
+ } else if (skipSmallSplit && typeof (split) == 'string') {
201
+ let smallSplit = split.split('=');
202
+ keys = [{ key: smallSplit[0], val: smallSplit[1] }]
203
+ }
204
+
205
+ let allowed = true;
206
+ queryParams.forEach((val, key) => {
207
+ let check = keys.find((value) => value.key == key && value.val == val);
208
+ if (!check) allowed = false;
209
+ })
210
+
211
+ return allowed
212
+ }
213
+
214
+ // ============================================================================
215
+ // Event Record Utilities
216
+ // ============================================================================
217
+
218
+ export function updateEventRecord(trackedEvent: Record<string, string> | PageEvent | PageEventCore) {
219
+ if (window.lytxDataLayer && window.lytxDataLayer.length > 1) {
220
+ window.lytxDataLayer.find((layer) => layer.tag == window.lytxApi.currentSiteConfig.tag)?.tracked.push(trackedEvent as any);
221
+ } else {
222
+ window.lytxDataLayer[0].tracked.push(trackedEvent as any);
223
+ }
224
+ }
225
+
226
+ export function trackCustomRecord(event: PageEvent | PageEventCore) {
227
+ if (window.lytxApi.track_web_events) {
228
+ window.lytxApi.trackCustomEvents(
229
+ window.lytxApi.currentSiteConfig.tag,
230
+ window.lytxApi.platform,
231
+ { custom: event.event_name },
232
+ "",
233
+ {
234
+ type: "custom",
235
+ name: event.event_name,
236
+ condition: event.condition,
237
+ rules: event.rules,
238
+ parameters: event.parameters,
239
+ }
240
+ );
241
+ if (window.lytxApi.debugMode) {
242
+ console.trace('Event tracked is', event.event_name);
243
+ }
244
+ }
245
+ }
246
+
247
+ // ============================================================================
248
+ // Parameter Handling
249
+ // ============================================================================
250
+
251
+ export function handleParameters(ev: PageEvent | PageEventCore) {
252
+ const { parameters } = ev;
253
+ if (parameters.startsWith(".")) {
254
+ return parameters;
255
+ }
256
+ if (parameters.startsWith("#")) {
257
+ return parameters;
258
+ }
259
+ return `[${ev.parameters}]`;
260
+ }
261
+
262
+ export function handleParseConfig(ev: PageEvent | PageEventCore) {
263
+ const { paramConfig } = ev;
264
+
265
+ let parseConfig: paramConfig | null = null;
266
+ let iframe = false;
267
+ const delay = { enabled: false, amount: 0 };
268
+
269
+ if (paramConfig) {
270
+ try {
271
+ if (ev.paramConfig.includes('&quot;')) {
272
+ let rawStr = ev.paramConfig.replaceAll('&quot;', '"');
273
+ parseConfig = JSON.parse(rawStr);
274
+ } else {
275
+ parseConfig = JSON.parse(ev.paramConfig) as paramConfig;
276
+ }
277
+ } catch (error) {
278
+ console.warn(error);
279
+ }
280
+ if (parseConfig) {
281
+ if (parseConfig.delay) {
282
+ delay.amount = Number(parseConfig.delay);
283
+ delay.enabled = true;
284
+ }
285
+ if (parseConfig.iframe) {
286
+ iframe = true;
287
+ }
288
+ }
289
+ }
290
+ return { parseConfig, iframe, delay };
291
+ }
292
+
293
+ // ============================================================================
294
+ // DOM Observer Utilities
295
+ // ============================================================================
296
+
297
+ export function waitForAllElements(
298
+ selector: string,
299
+ callback: (elements: Array<Element>) => void,
300
+ options = {
301
+ timeout: 10000,
302
+ targetNode: document.body,
303
+ observerOptions: { childList: true, subtree: true }
304
+ }
305
+ ) {
306
+ const nodes = document.querySelectorAll(selector);
307
+ const elements = Array.from(nodes);
308
+ if (elements.length > 0) {
309
+ callback(elements);
310
+ if (window.lytxApi.debugMode) {
311
+ console.log(`Element list found for ${selector} see list here`, elements);
312
+ }
313
+ return;
314
+ }
315
+ if (window.lytxApi.debugMode) {
316
+ console.log(`Element list not found for ${selector}`);
317
+ }
318
+
319
+ const timeoutId = setTimeout(() => {
320
+ if (observer) observer.disconnect();
321
+ }, options.timeout);
322
+
323
+ let throttled = false;
324
+ const checkForElement = () => {
325
+ if (throttled) return;
326
+ throttled = true;
327
+
328
+ setTimeout(() => {
329
+ const nodes = document.querySelectorAll(selector);
330
+ const elements = Array.from(nodes);
331
+
332
+ if (elements.length > 0) {
333
+ if (window.lytxApi.debugMode) {
334
+ console.log(`Element list found for ${selector} after initial delay see list here`, elements);
335
+ }
336
+ observer.disconnect();
337
+ clearTimeout(timeoutId);
338
+ callback(elements);
339
+ }
340
+ throttled = false;
341
+ }, 50);
342
+ };
343
+
344
+ const observer = new MutationObserver(checkForElement);
345
+ observer.observe(options.targetNode, options.observerOptions);
346
+ }
347
+
348
+ export function waitForElement(
349
+ selector: string,
350
+ callback: (element: Element) => void,
351
+ options = {
352
+ timeout: 10000,
353
+ targetNode: document.body,
354
+ observerOptions: { childList: true, subtree: true }
355
+ }
356
+ ) {
357
+ const element = document.querySelector(selector);
358
+ if (element) {
359
+ callback(element);
360
+ if (window.lytxApi.debugMode) {
361
+ console.log(`Element found for ${selector}`, element);
362
+ }
363
+ return;
364
+ }
365
+ if (window.lytxApi.debugMode) {
366
+ console.log(`Element not found for ${selector}`);
367
+ }
368
+
369
+ const timeoutId = setTimeout(() => {
370
+ if (observer) observer.disconnect();
371
+ }, options.timeout);
372
+
373
+ let throttled = false;
374
+ const checkForElement = () => {
375
+ if (throttled) return;
376
+ throttled = true;
377
+
378
+ setTimeout(() => {
379
+ const element = document.querySelector(selector);
380
+ if (element) {
381
+ if (window.lytxApi.debugMode) {
382
+ console.log(`Element found for ${selector} after initial delay`, element);
383
+ }
384
+ observer.disconnect();
385
+ clearTimeout(timeoutId);
386
+ callback(element);
387
+ }
388
+ throttled = false;
389
+ }, 50);
390
+ };
391
+
392
+ const observer = new MutationObserver(checkForElement);
393
+ observer.observe(options.targetNode, options.observerOptions);
394
+ }
395
+
396
+ // ============================================================================
397
+ // Autocapture Utilities
398
+ // ============================================================================
399
+
400
+ /** Generate a unique key for an element to track duplicates */
401
+ function getElementKey(element: HTMLElement, eventType: string): string {
402
+ const text = element.textContent?.trim().slice(0, 50) || '';
403
+ const href = (element as HTMLAnchorElement).href || '';
404
+ const id = element.id || '';
405
+ const className = element.className || '';
406
+ return `${eventType}:${text}:${href}:${id}:${className}`;
407
+ }
408
+
409
+ /** Get descriptive text for an element */
410
+ function getElementText(element: HTMLElement): string {
411
+ // For links/buttons, prefer aria-label, then text content
412
+ return (
413
+ element.getAttribute('aria-label') ||
414
+ element.textContent?.trim().slice(0, 100) ||
415
+ element.getAttribute('title') ||
416
+ ''
417
+ );
418
+ }
419
+
420
+ /** Get element type description */
421
+ function getElementType(element: HTMLElement): string {
422
+ const tagName = element.tagName.toLowerCase();
423
+ if (tagName === 'a') return 'link';
424
+ if (tagName === 'button') return 'button';
425
+ if (tagName === 'input') {
426
+ const type = (element as HTMLInputElement).type;
427
+ if (type === 'submit') return 'submit_button';
428
+ if (type === 'button') return 'button';
429
+ }
430
+ if (tagName === 'form') return 'form';
431
+ return tagName;
432
+ }
433
+
434
+ /** Initialize autocapture for clicks and form submissions */
435
+ function initAutocapture(config: SiteConfig, platformName: Platform) {
436
+ const debug = window.lytxApi?.debugMode;
437
+
438
+ // Track which elements have been captured in this session to avoid rapid duplicates
439
+ const capturedThisSession = new Set<string>();
440
+
441
+ if (debug) {
442
+ console.log('๐ŸŽฏ Lytx Autocapture enabled');
443
+ }
444
+
445
+ // Capture clicks on links and buttons
446
+ document.addEventListener('click', (e) => {
447
+ const target = e.target as HTMLElement;
448
+
449
+ // Find the closest interactive element
450
+ const anchor = target.closest('a') as HTMLAnchorElement | null;
451
+ const button = target.closest('button, input[type="submit"], input[type="button"]') as HTMLElement | null;
452
+
453
+ const element = anchor || button;
454
+ if (!element) return;
455
+
456
+ const elementType = getElementType(element);
457
+ const elementText = getElementText(element);
458
+ const elementKey = getElementKey(element, 'click');
459
+
460
+ // Skip if this exact interaction was just captured (debounce rapid clicks)
461
+ if (capturedThisSession.has(elementKey)) {
462
+ if (debug) {
463
+ console.log('๐ŸŽฏ Autocapture skipped (duplicate):', elementType, elementText);
464
+ }
465
+ return;
466
+ }
467
+
468
+ // Build event name with element info: $ac_link_ElementText_elementId
469
+ // Sanitize text for event name (remove special chars, limit length)
470
+ const sanitizedText = elementText
471
+ .replace(/[^a-zA-Z0-9\s]/g, '')
472
+ .trim()
473
+ .slice(0, 30)
474
+ .replace(/\s+/g, ' ');
475
+
476
+ const elementId = element.id || null;
477
+ const eventNameParts = [`$ac`, elementType, sanitizedText || 'unnamed'];
478
+ if (elementId) {
479
+ eventNameParts.push(elementId);
480
+ }
481
+ const eventName = eventNameParts.join('_');
482
+
483
+ // Check if user has manually captured this event name - if so, skip autocapture
484
+ if (window.lytxApi._manualCaptures.has(eventName)) {
485
+ if (debug) {
486
+ console.log('๐ŸŽฏ Autocapture skipped (manual capture exists):', eventName);
487
+ }
488
+ return;
489
+ }
490
+
491
+ // Mark as captured (with TTL to allow re-capture after some time)
492
+ capturedThisSession.add(elementKey);
493
+ setTimeout(() => capturedThisSession.delete(elementKey), 1000);
494
+
495
+ const customData: Record<string, string> = {
496
+ autocapture: 'true',
497
+ element_type: elementType,
498
+ element_text: elementText,
499
+ page_path: window.location.pathname,
500
+ page_url: window.location.href,
501
+ };
502
+
503
+ // Add href for links
504
+ if (anchor?.href) {
505
+ customData.link_url = anchor.href;
506
+ }
507
+
508
+ // Add element identifiers if available
509
+ if (elementId) {
510
+ customData.element_id = elementId;
511
+ }
512
+ if (element.className && typeof element.className === 'string') {
513
+ customData.element_classes = element.className.split(' ').slice(0, 5).join(' ');
514
+ }
515
+
516
+ if (debug) {
517
+ console.log('๐ŸŽฏ Autocapture click:', eventName, customData);
518
+ }
519
+
520
+ trackEvents(config.tag, platformName, { custom: eventName }, "", customData);
521
+ }, true); // Use capture phase to get events before they might be stopped
522
+
523
+ // Capture form submissions
524
+ document.addEventListener('submit', (e) => {
525
+ const form = e.target as HTMLFormElement;
526
+ if (!form || form.tagName.toLowerCase() !== 'form') return;
527
+
528
+ const formName = form.getAttribute('name') || form.id || 'unnamed_form';
529
+ const formAction = form.action || window.location.href;
530
+ const elementKey = getElementKey(form, 'submit');
531
+
532
+ // Skip if this exact form was just submitted
533
+ if (capturedThisSession.has(elementKey)) {
534
+ if (debug) {
535
+ console.log('๐ŸŽฏ Autocapture skipped (duplicate form submit):', formName);
536
+ }
537
+ return;
538
+ }
539
+
540
+ // Build event name with form info: $ac_form_FormName_formId
541
+ const sanitizedFormName = formName
542
+ .replace(/[^a-zA-Z0-9\s]/g, '')
543
+ .trim()
544
+ .slice(0, 30)
545
+ .replace(/\s+/g, ' ');
546
+
547
+ const formId = form.id || null;
548
+ const eventNameParts = ['$ac', 'form', sanitizedFormName || 'unnamed'];
549
+ if (formId && formId !== formName) {
550
+ eventNameParts.push(formId);
551
+ }
552
+ const eventName = eventNameParts.join('_');
553
+
554
+ if (window.lytxApi._manualCaptures.has(eventName)) {
555
+ if (debug) {
556
+ console.log('๐ŸŽฏ Autocapture skipped (manual capture exists):', eventName);
557
+ }
558
+ return;
559
+ }
560
+
561
+ capturedThisSession.add(elementKey);
562
+ setTimeout(() => capturedThisSession.delete(elementKey), 1000);
563
+
564
+ const customData: Record<string, string> = {
565
+ autocapture: 'true',
566
+ element_type: 'form',
567
+ form_name: formName,
568
+ form_action: formAction,
569
+ form_method: form.method || 'get',
570
+ page_path: window.location.pathname,
571
+ page_url: window.location.href,
572
+ };
573
+
574
+ if (formId) {
575
+ customData.element_id = formId;
576
+ }
577
+
578
+ if (debug) {
579
+ console.log('๐ŸŽฏ Autocapture form submit:', eventName, customData);
580
+ }
581
+
582
+ trackEvents(config.tag, platformName, { custom: eventName }, "", customData);
583
+ }, true);
584
+ }
585
+
586
+ // ============================================================================
587
+ // Main parseData Function
588
+ // ============================================================================
589
+
590
+ export function createParseData(
591
+ handleCondition: (ev: PageEvent | PageEventCore, url: URL) => void,
592
+ versionLabel: string = ""
593
+ ) {
594
+ return function parseData(
595
+ data: (PageEvent | PageEventCore)[] | null = null,
596
+ config: SiteConfig,
597
+ track_web_events: boolean,
598
+ platformName: Platform
599
+ ) {
600
+ const pageUrl = new URL(window.location.href);
601
+ const debug = pageUrl.searchParams.has('lytxDebug');
602
+ if (window.lytxDataLayer.length < 2) {
603
+ console.log(`Lytx script is working ๐Ÿ”ฅ๐Ÿ”ฅ๐Ÿ”ฅ${debug ? '๐Ÿ›๐Ÿ›๐Ÿ› debug enabled' : ''}${versionLabel}`);
604
+ }
605
+
606
+ function loadLytxEvents(url: URL = new URL(window.location.href)) {
607
+ if (data) {
608
+ if (window.lytxDataLayer.length < 2) {
609
+ console.log(
610
+ "โšกโšกโšกโšก See Defined Lytx Events Here โšกโšกโšกโšก -->",
611
+ window.lytxDataLayer
612
+ );
613
+ }
614
+ try {
615
+ for (const ev of data) {
616
+ handleCondition(ev, url);
617
+ }
618
+ } catch (error) {
619
+ console.log(error);
620
+ }
621
+ }
622
+ }
623
+
624
+ // Track manual captures to avoid autocapture duplicates
625
+ const manualCaptures = new Set<string>();
626
+
627
+ window.lytxApi = {
628
+ emit: loadLytxEvents,
629
+ event: trackEvents,
630
+ capture: (eventName: string, customData?: Record<string, string>) => {
631
+ // Track this as a manual capture to prevent autocapture duplication
632
+ manualCaptures.add(eventName);
633
+ trackEvents(config.tag, platformName, { custom: eventName }, "", customData);
634
+ },
635
+ debugMode: debug,
636
+ platform: platformName,
637
+ currentSiteConfig: config,
638
+ track_web_events: track_web_events,
639
+ trackCustomEvents: trackEvents,
640
+ _manualCaptures: manualCaptures,
641
+ rid: () => {
642
+ if (window.lytxDataLayer) {
643
+ let ridVal = null;
644
+ window.lytxDataLayer.forEach((layer) => {
645
+ if (layer.rid) {
646
+ ridVal = layer.rid;
647
+ }
648
+ });
649
+ return ridVal;
650
+ } else {
651
+ return null;
652
+ }
653
+ }
654
+ }
655
+ loadLytxEvents();
656
+
657
+ // Initialize autocapture if enabled
658
+ if (config.autocapture && track_web_events) {
659
+ initAutocapture(config, platformName);
660
+ }
661
+ }
662
+ }