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.
- package/.env.example +37 -0
- package/README.md +486 -0
- package/alchemy.run.ts +155 -0
- package/cli/bootstrap-admin.ts +284 -0
- package/cli/deploy-staging.ts +692 -0
- package/cli/import-events.ts +628 -0
- package/cli/import-sites.ts +518 -0
- package/cli/index.ts +609 -0
- package/cli/init-db.ts +269 -0
- package/cli/migrate-to-durable-objects.ts +564 -0
- package/cli/migration-worker.ts +300 -0
- package/cli/performance-test.ts +588 -0
- package/cli/pg/client.ts +4 -0
- package/cli/pg/new-site.ts +153 -0
- package/cli/rollback-durable-objects.ts +622 -0
- package/cli/seed-data.ts +459 -0
- package/cli/setup.js +18 -0
- package/cli/setup.ts +463 -0
- package/cli/validate-migration.ts +200 -0
- package/cli/wrangler-migration.jsonc +28 -0
- package/db/adapter.ts +166 -0
- package/db/analytics_engine/client.ts +0 -0
- package/db/analytics_engine/sites.ts +0 -0
- package/db/client.ts +16 -0
- package/db/d1/client.ts +8 -0
- package/db/d1/drizzle.config.ts +35 -0
- package/db/d1/migrations/0000_true_maelstrom.sql +165 -0
- package/db/d1/migrations/0001_wonderful_bloodaxe.sql +12 -0
- package/db/d1/migrations/0002_late_frightful_four.sql +1 -0
- package/db/d1/migrations/0003_cuddly_obadiah_stane.sql +16 -0
- package/db/d1/migrations/0004_mute_stardust.sql +1 -0
- package/db/d1/migrations/0005_awesome_silvermane.sql +3 -0
- package/db/d1/migrations/0006_volatile_shriek.sql +2 -0
- package/db/d1/migrations/0007_superb_lila_cheney.sql +1 -0
- package/db/d1/migrations/0008_bitter_longshot.sql +17 -0
- package/db/d1/migrations/0009_wonderful_madame_masque.sql +28 -0
- package/db/d1/migrations/meta/0000_snapshot.json +1112 -0
- package/db/d1/migrations/meta/0001_snapshot.json +1187 -0
- package/db/d1/migrations/meta/0002_snapshot.json +1194 -0
- package/db/d1/migrations/meta/0003_snapshot.json +1296 -0
- package/db/d1/migrations/meta/0004_snapshot.json +1303 -0
- package/db/d1/migrations/meta/0005_snapshot.json +1325 -0
- package/db/d1/migrations/meta/0006_snapshot.json +1339 -0
- package/db/d1/migrations/meta/0007_snapshot.json +1347 -0
- package/db/d1/migrations/meta/0008_snapshot.json +1464 -0
- package/db/d1/migrations/meta/0009_snapshot.json +1648 -0
- package/db/d1/migrations/meta/_journal.json +76 -0
- package/db/d1/schema.ts +407 -0
- package/db/d1/sites.ts +374 -0
- package/db/d1/teamAiUsage.ts +101 -0
- package/db/d1/teams.ts +127 -0
- package/db/durable/drizzle.config.ts +8 -0
- package/db/durable/durableObjectClient.ts +480 -0
- package/db/durable/events.ts +100 -0
- package/db/durable/migrations/0000_fair_bucky.sql +38 -0
- package/db/durable/migrations/meta/0000_snapshot.json +278 -0
- package/db/durable/migrations/meta/_journal.json +13 -0
- package/db/durable/migrations/migrations.js +10 -0
- package/db/durable/schema.ts +5 -0
- package/db/durable/siteDurableObject.ts +1352 -0
- package/db/durable/types.ts +53 -0
- package/db/postgres/client.ts +13 -0
- package/db/postgres/drizzle.config.ts +12 -0
- package/db/postgres/migrations/0000_brainy_sprite.sql +116 -0
- package/db/postgres/migrations/meta/0000_snapshot.json +681 -0
- package/db/postgres/migrations/meta/_journal.json +13 -0
- package/db/postgres/schema.ts +145 -0
- package/db/postgres/sites.ts +118 -0
- package/db/tranformReports.ts +595 -0
- package/db/types.ts +55 -0
- package/endpoints/api_worker.tsx +1854 -0
- package/endpoints/site_do_worker.ts +11 -0
- package/index.d.ts +63 -0
- package/index.ts +83 -0
- package/lib/auth.ts +279 -0
- package/lib/geojson/world_countries.json +45307 -0
- package/lib/random_name.ts +41 -0
- package/lib/sendMail.ts +252 -0
- package/package.json +142 -0
- package/public/favicon.ico +0 -0
- package/public/images/android-chrome-192x192.png +0 -0
- package/public/images/android-chrome-512x512.png +0 -0
- package/public/images/apple-touch-icon.png +0 -0
- package/public/images/favicon-16x16.png +0 -0
- package/public/images/favicon-32x32.png +0 -0
- package/public/images/lytx_dark_dashboard.png +0 -0
- package/public/images/lytx_light_dashboard.png +0 -0
- package/public/images/safari-pinned-tab.svg +4 -0
- package/public/logo.png +0 -0
- package/public/site.webmanifest +26 -0
- package/public/sw.js +107 -0
- package/src/Document.tsx +86 -0
- package/src/api/ai_api.ts +1156 -0
- package/src/api/authMiddleware.ts +45 -0
- package/src/api/auth_api.ts +465 -0
- package/src/api/event_labels_api.ts +193 -0
- package/src/api/events_api.ts +210 -0
- package/src/api/queueWorker.ts +303 -0
- package/src/api/reports_api.ts +278 -0
- package/src/api/seed_api.ts +288 -0
- package/src/api/sites_api.ts +904 -0
- package/src/api/tag_api.ts +458 -0
- package/src/api/tag_api_v2.ts +289 -0
- package/src/api/team_api.ts +456 -0
- package/src/app/Dashboard.tsx +1339 -0
- package/src/app/Events.tsx +974 -0
- package/src/app/Explore.tsx +312 -0
- package/src/app/Layout.tsx +58 -0
- package/src/app/Settings.tsx +1302 -0
- package/src/app/components/DashboardCard.tsx +118 -0
- package/src/app/components/EditableCell.tsx +123 -0
- package/src/app/components/EventForm.tsx +93 -0
- package/src/app/components/MarketingFooter.tsx +49 -0
- package/src/app/components/MarketingNav.tsx +150 -0
- package/src/app/components/Nav.tsx +755 -0
- package/src/app/components/NewSiteSetup.tsx +298 -0
- package/src/app/components/SQLEditor.tsx +740 -0
- package/src/app/components/SiteSelector.tsx +126 -0
- package/src/app/components/SiteTag.tsx +42 -0
- package/src/app/components/SiteTagInstallCard.tsx +241 -0
- package/src/app/components/WorldMapCard.tsx +337 -0
- package/src/app/components/charts/ChartComponents.tsx +1481 -0
- package/src/app/components/charts/EventFunnel.tsx +45 -0
- package/src/app/components/charts/EventSummary.tsx +194 -0
- package/src/app/components/charts/SankeyFlows.tsx +72 -0
- package/src/app/components/marketing/CheckIcon.tsx +16 -0
- package/src/app/components/marketing/MarketingLayout.tsx +23 -0
- package/src/app/components/marketing/SectionHeading.tsx +35 -0
- package/src/app/components/reports/AskAiWorkspace.tsx +371 -0
- package/src/app/components/reports/CreateReportStarter.tsx +74 -0
- package/src/app/components/reports/DashboardRouteFiltersContext.tsx +14 -0
- package/src/app/components/reports/DashboardToolbar.tsx +154 -0
- package/src/app/components/reports/DashboardWorkspaceLayout.tsx +63 -0
- package/src/app/components/reports/DashboardWorkspaceShell.tsx +118 -0
- package/src/app/components/reports/ReportBuilderWorkspace.tsx +76 -0
- package/src/app/components/reports/custom/CustomReportBuilderPage.tsx +1667 -0
- package/src/app/components/reports/custom/ReportWidgetChart.tsx +297 -0
- package/src/app/components/reports/custom/buildWidgetSql.ts +151 -0
- package/src/app/components/reports/custom/chartPalettes.ts +18 -0
- package/src/app/components/reports/custom/types.ts +50 -0
- package/src/app/components/reports/reportBuilderMenuItems.ts +17 -0
- package/src/app/components/reports/useDashboardToolbarControls.tsx +235 -0
- package/src/app/components/ui/AlertBanner.tsx +101 -0
- package/src/app/components/ui/Button.tsx +55 -0
- package/src/app/components/ui/Card.tsx +80 -0
- package/src/app/components/ui/Input.tsx +72 -0
- package/src/app/components/ui/Link.tsx +23 -0
- package/src/app/components/ui/ReportBuilderMenu.tsx +246 -0
- package/src/app/components/ui/ThemeToggle.tsx +54 -0
- package/src/app/constants.ts +6 -0
- package/src/app/headers.ts +33 -0
- package/src/app/providers/AuthProvider.tsx +189 -0
- package/src/app/providers/ClientProviders.tsx +18 -0
- package/src/app/providers/QueryProvider.tsx +23 -0
- package/src/app/providers/ThemeProvider.tsx +88 -0
- package/src/app/utils/chartThemes.ts +146 -0
- package/src/app/utils/keybinds.ts +96 -0
- package/src/app/utils/media.tsx +24 -0
- package/src/client.tsx +114 -0
- package/src/config/createLytxAppConfig.ts +252 -0
- package/src/config/resourceNames.ts +88 -0
- package/src/db/index.ts +67 -0
- package/src/index.css +285 -0
- package/src/lib/featureFlags.ts +69 -0
- package/src/pages/GetStarted.tsx +290 -0
- package/src/pages/Home.tsx +268 -0
- package/src/pages/Login.tsx +283 -0
- package/src/pages/PrivacyPolicy.tsx +120 -0
- package/src/pages/Signup.tsx +267 -0
- package/src/pages/TermsOfService.tsx +126 -0
- package/src/pages/VerifyEmail.tsx +56 -0
- package/src/session/durableObject.ts +7 -0
- package/src/session/siteSchema.ts +86 -0
- package/src/session/types.ts +36 -0
- package/src/templates/README.md +80 -0
- package/src/templates/cleanFunctions.js +44 -0
- package/src/templates/embedFunctions.js +52 -0
- package/src/templates/lytx-shared.ts +662 -0
- package/src/templates/lytxpixel-core.ts +144 -0
- package/src/templates/lytxpixel.ts +267 -0
- package/src/templates/lytxpixelBrowser.js +634 -0
- package/src/templates/lytxpixelBrowser.mjs +634 -0
- package/src/templates/parseData.js +12 -0
- package/src/templates/script.ts +31 -0
- package/src/templates/template.tsx +50 -0
- package/src/templates/test.js +3 -0
- package/src/templates/trackWebEvents.ts +177 -0
- package/src/templates/vendors/clickcease.ts +8 -0
- package/src/templates/vendors/google.ts +174 -0
- package/src/templates/vendors/linkedin.ts +23 -0
- package/src/templates/vendors/meta.ts +56 -0
- package/src/templates/vendors/quantcast.ts +22 -0
- package/src/templates/vendors/simplfi.ts +7 -0
- package/src/types/app-context.ts +16 -0
- package/src/utilities/dashboardParams.ts +188 -0
- package/src/utilities/dashboardQueries.ts +537 -0
- package/src/utilities/dashboardTransforms.ts +167 -0
- package/src/utilities/dataValidation.ts +414 -0
- package/src/utilities/detector.ts +73 -0
- package/src/utilities/encrypt.ts +103 -0
- package/src/utilities/index.ts +13 -0
- package/src/utilities/parser.ts +117 -0
- package/src/utilities/performanceMonitoring.ts +570 -0
- package/src/utilities/route_interuptors.ts +24 -0
- package/src/worker.tsx +675 -0
- package/tsconfig.json +78 -0
- package/types/env.d.ts +16 -0
- package/types/rw.d.ts +7 -0
- package/types/shims.d.ts +53 -0
- package/types/vite.d.ts +19 -0
- package/vite/vite-plugin-pixel-bundle.ts +126 -0
- package/vite.config.ts +53 -0
- 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(/'/g, "'")
|
|
163
|
+
.replaceAll(/"/g, '"')
|
|
164
|
+
.replaceAll(/>/g, '>')
|
|
165
|
+
.replaceAll(/</g, '<')
|
|
166
|
+
.replaceAll(/&/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('"')) {
|
|
272
|
+
let rawStr = ev.paramConfig.replaceAll('"', '"');
|
|
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
|
+
}
|