ec4w_validator 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.idx/dev.nix ADDED
@@ -0,0 +1,34 @@
1
+ { pkgs }: {
2
+ channel = "stable-25.05";
3
+ packages = [
4
+ pkgs.nodejs_24
5
+ # pkgs.deno
6
+ ];
7
+ env = { };
8
+ idx.extensions = [
9
+ # "vscodevim.vim"
10
+ "ms-vscode.js-debug"
11
+ "usernamehw.errorlens"
12
+ "dbaeumer.vscode-eslint"
13
+ "formulahendry.auto-rename-tag"
14
+ "esbenp.prettier-vscode"
15
+ "mermaidchart.vscode-mermaid-chart"
16
+ "google.gemini-cli-vscode-ide-companion"
17
+ ];
18
+ idx = {
19
+ workspace = {
20
+ # Runs when a workspace is first created
21
+ onCreate = {
22
+ # Example: install JS dependencies from NPM
23
+ # npm-install = "npm install";
24
+ # Open editors for the following files by default, if they exist:
25
+ # default.openFiles = [ "style.css" "main.js" "index.html" ];
26
+ };
27
+ # Runs when the workspace is (re)started
28
+ onStart = {
29
+ # Example: start a background task to watch and re-build backend code
30
+ # watch-backend = "npm run watch-backend";
31
+ };
32
+ };
33
+ };
34
+ }
@@ -0,0 +1,168 @@
1
+ // Place your key bindings in this file to override the defaultsauto[]
2
+ [
3
+ {
4
+ "key": "ctrl+t",
5
+ "command": "workbench.action.files.newUntitledFile"
6
+ },
7
+ {
8
+ "key": "ctrl+n",
9
+ "command": "-workbench.action.files.newUntitledFile"
10
+ },
11
+ {
12
+ "key": "ctrl+n",
13
+ "command": "explorer.newFile"
14
+ },
15
+ {
16
+ "key": "ctrl+shift+alt+/",
17
+ "command": "workbench.action.openSettings"
18
+ },
19
+ {
20
+ "key": "ctrl+,",
21
+ "command": "-workbench.action.openSettings"
22
+ },
23
+ {
24
+ "key": "alt+9",
25
+ "command": "-workbench.action.openEditorAtIndex9"
26
+ },
27
+ {
28
+ "key": "alt+8",
29
+ "command": "-workbench.action.openEditorAtIndex8"
30
+ },
31
+ {
32
+ "key": "alt+7",
33
+ "command": "-workbench.action.openEditorAtIndex7"
34
+ },
35
+ {
36
+ "key": "alt+6",
37
+ "command": "-workbench.action.openEditorAtIndex6"
38
+ },
39
+ {
40
+ "key": "alt+5",
41
+ "command": "-workbench.action.openEditorAtIndex5"
42
+ },
43
+ {
44
+ "key": "alt+4",
45
+ "command": "-workbench.action.openEditorAtIndex4"
46
+ },
47
+ {
48
+ "key": "alt+3",
49
+ "command": "-workbench.action.openEditorAtIndex3"
50
+ },
51
+ {
52
+ "key": "alt+2",
53
+ "command": "-workbench.action.openEditorAtIndex2"
54
+ },
55
+ {
56
+ "key": "alt+1",
57
+ "command": "-workbench.action.openEditorAtIndex1"
58
+ },
59
+ {
60
+ "key": "shift+pageup",
61
+ "command": "editor.action.insertCursorAbove",
62
+ "when": "editorTextFocus"
63
+ },
64
+ {
65
+ "key": "ctrl+shift+up",
66
+ "command": "-editor.action.insertCursorAbove",
67
+ "when": "editorTextFocus"
68
+ },
69
+ {
70
+ "key": "shift+pagedown",
71
+ "command": "editor.action.insertCursorBelow",
72
+ "when": "editorTextFocus"
73
+ },
74
+ {
75
+ "key": "ctrl+shift+down",
76
+ "command": "-editor.action.insertCursorBelow",
77
+ "when": "editorTextFocus"
78
+ },
79
+ {
80
+ "key": "pagedown",
81
+ "command": "editor.action.moveLinesDownAction",
82
+ "when": "editorTextFocus && !editorReadonly"
83
+ },
84
+ {
85
+ "key": "alt+down",
86
+ "command": "-editor.action.moveLinesDownAction",
87
+ "when": "editorTextFocus && !editorReadonly"
88
+ },
89
+ {
90
+ "key": "pageup",
91
+ "command": "editor.action.moveLinesUpAction",
92
+ "when": "editorTextFocus && !editorReadonly"
93
+ },
94
+ {
95
+ "key": "alt+up",
96
+ "command": "-editor.action.moveLinesUpAction",
97
+ "when": "editorTextFocus && !editorReadonly"
98
+ },
99
+ {
100
+ "key": "shift+alt+f",
101
+ "command": "editor.action.formatDocument",
102
+ "when": "editorHasDocumentFormattingProvider && editorTextFocus && !editorReadonly && !inCompositeEditor"
103
+ },
104
+ {
105
+ "key": "ctrl+shift+i",
106
+ "command": "-editor.action.formatDocument",
107
+ "when": "editorHasDocumentFormattingProvider && editorTextFocus && !editorReadonly && !inCompositeEditor"
108
+ },
109
+ {
110
+ "key": "shift+alt+f",
111
+ "command": "editor.action.formatDocument.none",
112
+ "when": "editorTextFocus && !editorHasDocumentFormattingProvider && !editorReadonly"
113
+ },
114
+ {
115
+ "key": "ctrl+shift+i",
116
+ "command": "-editor.action.formatDocument.none",
117
+ "when": "editorTextFocus && !editorHasDocumentFormattingProvider && !editorReadonly"
118
+ },
119
+ {
120
+ "key": "ctrl+shift+i",
121
+ "command": "editor.action.formatSelection.multiple"
122
+ },
123
+ {
124
+ "key": "ctrl+shift+alt+c",
125
+ "command": "-copyRelativeFilePath",
126
+ "when": "!editorFocus"
127
+ },
128
+ {
129
+ "key": "ctrl+k ctrl+shift+alt+c",
130
+ "command": "-copyRelativeFilePath",
131
+ "when": "editorFocus"
132
+ },
133
+ {
134
+ "key": "ctrl+alt+c",
135
+ "command": "-search.action.copyPath",
136
+ "when": "fileMatchOrFolderMatchWithResourceFocus"
137
+ },
138
+ {
139
+ "key": "ctrl+alt+c",
140
+ "command": "-copyFilePath",
141
+ "when": "!editorFocus"
142
+ },
143
+ {
144
+ "key": "ctrl+k ctrl+alt+c",
145
+ "command": "-copyFilePath",
146
+ "when": "editorFocus"
147
+ },
148
+ {
149
+ "key": "ctrl+v",
150
+ "command": "-notebook.cell.paste",
151
+ "when": "notebookEditorFocused && !inputFocus"
152
+ },
153
+ {
154
+ "key": "ctrl+v",
155
+ "command": "-filesExplorer.paste",
156
+ "when": "filesExplorerFocus && foldersViewVisible && !explorerResourceReadonly && !inputFocus"
157
+ },
158
+ {
159
+ "key": "ctrl+v",
160
+ "command": "editor.action.clipboardPasteAction",
161
+ "when": "editorTextFocus && !editorReadonly"
162
+ },
163
+ {
164
+ "key": "ctrl+v",
165
+ "command": "filesExplorer.paste",
166
+ "when": "filesExplorerFocus && !inputFocus"
167
+ }
168
+ ]
@@ -0,0 +1,32 @@
1
+ {
2
+ // Use IntelliSense to learn about possible attributes.
3
+ // Hover to view descriptions of existing attributes.
4
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5
+ "version": "0.2.0",
6
+ "configurations": [
7
+ {
8
+ "type": "node",
9
+ "request": "launch",
10
+ "name": "debug tests",
11
+ "skipFiles": [
12
+ "<node_internals>/**"
13
+ ],
14
+ "cwd": "${workspaceFolder}",
15
+ "runtimeExecutable": "deno",
16
+ "runtimeArgs": [
17
+ "test",
18
+ "--inspect-brk"
19
+ ],
20
+ "attachSimplePort": 9229
21
+ },
22
+ {
23
+ "type": "node",
24
+ "request": "launch",
25
+ "name": "Launch Program",
26
+ "skipFiles": [
27
+ "<node_internals>/**"
28
+ ],
29
+ "program": "${workspaceFolder}/src/ec4w_validator.js"
30
+ }
31
+ ]
32
+ }
@@ -0,0 +1,34 @@
1
+ import js from "@eslint/js";
2
+ import globals from "globals";
3
+ import { defineConfig } from "eslint/config";
4
+
5
+ export default defineConfig([
6
+ {
7
+ files: ["**/*.{js,mjs,cjs}"],
8
+ plugins: { js },
9
+ extends: ["js/recommended"],
10
+ languageOptions: {
11
+ globals: {
12
+ ...globals.browser,
13
+ // ...globals.webextensions,
14
+ },
15
+ ecmaVersion: 'latest',
16
+ },
17
+ rules: {
18
+ 'no-unused-vars': 'off',
19
+ 'no-undef': 'error',
20
+ 'no-unreachable': 'warn',
21
+ // 'no-case-declarations': 'off',
22
+ 'no-empty': 'warn',
23
+ 'no-invalid-this': 'error',
24
+ 'no-redeclare': 'warn',
25
+
26
+ // 'semi': ['warn', 'always'],
27
+ // 'eqeqeq': 'warn'
28
+ // 'quotes': ['off', 'single'],
29
+ },
30
+ // env: { // deprecated settings
31
+ // "chrome": true,
32
+ // },
33
+ },
34
+ ]);
package/index.html ADDED
@@ -0,0 +1,16 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8" />
6
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
7
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
8
+ <title>ec4w_validator</title>
9
+ </head>
10
+
11
+ <body>
12
+ <div id="app"></div>
13
+ <script type="module" src="/src/main.js"></script>
14
+ </body>
15
+
16
+ </html>
package/jsconfig.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "compilerOptions": {
3
+ "module": "es2022",
4
+ "target": "es2024",
5
+ "moduleResolution": "bundler",
6
+ "baseUrl": "./",
7
+ "paths": {
8
+ "/src/*": [
9
+ "./src/*"
10
+ ],
11
+ "@/*": [
12
+ "./src/*"
13
+ ]
14
+ },
15
+ "checkJs": true
16
+ },
17
+ "include": [
18
+ "./src/**/*.js",
19
+ "./src/**/*.mjs",
20
+ ],
21
+ "exclude": [
22
+ "./node_modules/",
23
+ "./**/*.md"
24
+ ],
25
+ }
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "ec4w_validator",
3
+ "private": false,
4
+ "version": "0.0.1",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "preview": "vite preview",
10
+ "test": "vitest --run",
11
+ "test-watch": "vitest",
12
+ "test-coverage": "vitest run --coverage"
13
+ },
14
+ "devDependencies": {
15
+ "@eslint/js": "^9.39.2",
16
+ "@vitest/coverage-v8": "^4.0.18",
17
+ "eslint": "^9.39.2",
18
+ "globals": "^17.2.0",
19
+ "jsdom": "^27.4.0",
20
+ "vite": "^7.2.4",
21
+ "vitest": "^4.0.18"
22
+ }
23
+ }
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
@@ -0,0 +1,318 @@
1
+ class EC4W_Validator {
2
+ /** @type {SentRequest[]} */
3
+ #requestList = [];
4
+
5
+ /** @param {SentRequest[]} requestList */
6
+ constructor(
7
+ requestList
8
+ ) {
9
+ this.#requestList = requestList;
10
+ }
11
+
12
+ /**
13
+ * @returns {EC4W_Result}
14
+ */
15
+ conclude() {
16
+
17
+ // map // transform
18
+ const sentRequests = [];
19
+ for (const request of this.#requestList) {
20
+ if (!request.url) continue;
21
+
22
+ const Url = new URL(request.url);
23
+ if (UPDE_Request.canBe(request.method, Url)) {
24
+ sentRequests.push(new UPDE_Request(request.method, Url));
25
+ }
26
+ else if (Conversion_Request.canBe(request.method, Url)) {
27
+ sentRequests.push(new Conversion_Request(request.method, Url));
28
+ }
29
+ else {
30
+ // skip
31
+ }
32
+ }
33
+
34
+
35
+ // groupping pairs
36
+ const EC4W_list = [];
37
+ {
38
+ let lastUPDE = null;
39
+ for (const ping of sentRequests) {
40
+ if (ping instanceof UPDE_Request) {
41
+ lastUPDE = ping;
42
+ }
43
+ else if (ping instanceof Conversion_Request) {
44
+ EC4W_list.push(new EC4W_Request(lastUPDE, ping));
45
+ }
46
+ }
47
+ }
48
+
49
+ return new EC4W_Result(EC4W_list);
50
+ }
51
+
52
+ /** @param {string} em */
53
+ static emIsValid(em) {
54
+ if (!em) return false;
55
+ if (em.startsWith('tv.1') === false) return false;
56
+ if (em === 'tv.1') return false;
57
+ if (/^tv\.1~e[0-9]/.test(em)) return false;
58
+ const part = em.split('~');
59
+ const len = part.length;
60
+ if (len < 2) return false;
61
+
62
+ for (let i = 1; i < len; ++i) {
63
+ if (part[i] === '') return false;
64
+ const [key, value] = part[i].split('.');
65
+ if (!key) return false;
66
+ if (!value) return false;
67
+ }
68
+
69
+ return true;
70
+ }
71
+
72
+ }
73
+
74
+ class EC4W_Request {
75
+ #UPDE_Request = null;
76
+ #Conversion_Request;
77
+
78
+ /**
79
+ * @param {UPDE_Request?} UPDE_Request
80
+ * @param {Conversion_Request} Conversion_Request */
81
+ constructor(
82
+ UPDE_Request,
83
+ Conversion_Request
84
+ ) {
85
+ this.#UPDE_Request = UPDE_Request;
86
+ this.#Conversion_Request = Conversion_Request;
87
+ }
88
+
89
+ get isValid() {
90
+ if (this.#Conversion_Request === null) return false;
91
+
92
+ if (this.#UPDE_Request === null) {
93
+ return this.#Conversion_Request.emIsValid();
94
+ }
95
+
96
+ if (this.#Conversion_Request.em === null) {
97
+ return this.#UPDE_Request.emIsValid();
98
+ }
99
+
100
+ return this.#Conversion_Request.emIsValid();
101
+ }
102
+ }
103
+
104
+ class UPDE_Request {
105
+ /**
106
+ * @param {string} method
107
+ * @param {URL} Url */
108
+ static canBe(method, Url) {
109
+ if (method !== 'GET') return false;
110
+ if (false === Url.toString().startsWith('https://www.google.com/ccm/form-data/')) return false;
111
+ if (Url.searchParams.get('em') === null) return false;
112
+ return true;
113
+ }
114
+
115
+ #method;
116
+ get method() { return this.#method; }
117
+ #Url;
118
+ get url() { return this.#Url.toString(); };
119
+
120
+ /**
121
+ * @param {string} method
122
+ * @param {URL} Url */
123
+ constructor(
124
+ method,
125
+ Url
126
+ ) {
127
+ this.#method = method;
128
+ this.#Url = Url;
129
+ }
130
+
131
+ get em() {
132
+ return this.#Url.searchParams.get('em');
133
+ }
134
+
135
+ emIsValid() {
136
+ return EC4W_Validator.emIsValid(this.em);
137
+ }
138
+
139
+ }
140
+
141
+ class Conversion_Request {
142
+
143
+ /**
144
+ * @param {string} method
145
+ * @param {URL} Url */
146
+ static canBe(method, Url) {
147
+ if (method !== 'GET') return false;
148
+ if (false === Url.toString().startsWith('https://www.googleadservices.com/ccm/conversion/')) return false;
149
+ if (Url.searchParams.get('label') === null) return false;
150
+ return true;
151
+ }
152
+
153
+ #method;
154
+ get method() { return this.#method; }
155
+ #Url;
156
+ get url() { return this.#Url.toString(); };
157
+
158
+ /**
159
+ * @param {string} method
160
+ * @param {URL} Url */
161
+ constructor(
162
+ method,
163
+ Url
164
+ ) {
165
+ this.#method = method;
166
+ this.#Url = Url;
167
+ }
168
+
169
+ get em() {
170
+ return this.#Url.searchParams.get('em');
171
+ }
172
+
173
+ emIsValid() {
174
+ return EC4W_Validator.emIsValid(this.em);
175
+ }
176
+ }
177
+
178
+
179
+ class EC4W_Result {
180
+ /** @type {EC4W_Request[]} */
181
+ EC4W_Requests = [];
182
+ // errors = [];
183
+ messages = [];
184
+
185
+ /**
186
+ * @param {EC4W_Request[]} EC4W_Requests
187
+ * */
188
+ constructor(
189
+ EC4W_Requests
190
+ ) {
191
+ this.EC4W_Requests = EC4W_Requests;
192
+
193
+ }
194
+
195
+ get invalidRequest() {
196
+ for (const EC4W_Request of this.EC4W_Requests) {
197
+ if (EC4W_Request.isValid === false) return EC4W_Request;
198
+ }
199
+ return null;
200
+ }
201
+
202
+ get summary() {
203
+ const summary = [];
204
+ for (const EC4W_Request of this.EC4W_Requests) {
205
+ summary.push({
206
+ isValid: EC4W_Request.isValid,
207
+ em: EC4W_Request.em,
208
+ });
209
+ }
210
+ return summary;
211
+ }
212
+
213
+ }
214
+
215
+
216
+
217
+ class SentRequest {
218
+ url = 'https://';
219
+ method = 'GET';
220
+
221
+ constructor(url, method) {
222
+ this.url = url;
223
+ this.method = method;
224
+ }
225
+ }
226
+
227
+
228
+
229
+
230
+ (function testmain() { // test
231
+
232
+ class fakeUPDE {
233
+ constructor(httpMethod, emValue, conversionId) {
234
+ this.conversionId = conversionId;
235
+ this.emValue = emValue;
236
+
237
+ const emParam = `&em=${emValue ? emValue : 'tv.1'}`;
238
+ this.url = `https://www.google.com/ccm/form-data/${conversionId}?gtm=45be61r1p3v9164572194za200zd9164572194xec&gcd=13l3l3l3l1l1&dma=0&tag_exp=103116026~103200004~104527907~104528501~104684208~104684211~115495939~115616985~115938465~115938468~116185181~116185182~116988316~117041587&gclaw_src=0_1&npa=0&frm=0&gclgs=12&gclst=63271790&gcllp=265921241&gclaw=TestingABC-2026-01-28&pscdl=noapi&auid=712372246.1762244434&uaa=x86&uab=64&uafvl=Google%2520Chrome%3B143.0.7499.203%7CChromium%3B143.0.7499.203%7CNot%2520A(Brand%3B24.0.0.0&uamb=0&uam=&uap=Chrome%20OS&uapv=16463.79.0&uaw=0&ec_mode=c&gap.plf=4.3&ecsid=1296144687.1769663220${emParam}`
239
+ this.method = httpMethod;
240
+ }
241
+ }
242
+
243
+ class fakeConve {
244
+ constructor(httpMethod, emValue, conversionId, conversionLabel) {
245
+ this.emValue = emValue;
246
+ this.conversionId = conversionId;
247
+ this.conversionLabel = conversionLabel;
248
+
249
+ const emParam = emValue ? `&em=${emValue}` : '';
250
+ this.url = `https://www.googleadservices.com/ccm/conversion/${conversionId}/?random=1769663226870&cv=11&fst=1769663226870&fmt=3&bg=ffffff&guid=ON&async=1&en=conversion&gtm=45be61r1v9164572194za200zd9164572194xec&gcd=13l3l3l3l1l1&dma=0&tag_exp=103116026~103200004~104527907~104528501~104684208~104684211~115495939~115616985~115938465~115938468~116185181~116185182~116988316~117041587&u_w=1745&u_h=1090&url=https%3A%2F%2Fgrcn-sme.github.io%2Fwordpress-example%2F&ref=https%3A%2F%2Fgrcn-sme.github.io%2Fwordpress-example%2F&gclaw_src=2_3&label=yYk9CMf--98YEMmV45Uq&capi=1&gtm_ee=1&frm=0&tiba=Wordpress%20Example&value=1&currency_code=MYR&hn=www.googleadservices.com&npa=0&gclgs=12&gclst=63274872&gcllp=265921241&gclaw=TestingABC-2026-01-28&pscdl=noapi&auid=712372246.1762244434&uaa=x86&uab=64&uafvl=Google%2520Chrome%3B143.0.7499.203%7CChromium%3B143.0.7499.203%7CNot%2520A(Brand%3B24.0.0.0&uamb=0&uam=&uap=Chrome%20OS&uapv=16463.79.0&uaw=0&ec_mode=c&oid=113729285.1769663227&oidsrc=3&ecsid=1296144687.1769663220&_tu=ABA&gcl_ctr=399~0&data=event%3Dconversion&gap.plf=4.3${emParam}`;
251
+ this.method = httpMethod;
252
+ }
253
+ }
254
+ const defaultEMvalue = 'tv.1~em.h5JGBrQTGorO7q6IaFMfu5cSqqB6XTp1aybOD11spnQ~pn.QizoLG_BckrIeAQvfQVWU6temD0YbmFoJqctQ4S2ivg';
255
+
256
+
257
+ function testValid(testName, inputRequests) {
258
+ console.log('==========');
259
+ console.log(testName);
260
+ try {
261
+ const sampleRequests = inputRequests || [
262
+ { url: 'https://www.google.com/ccm/form-data/11319954121?gtm=45be61r1p3v9164572194za200zd9164572194xec&gcd=13l3l3l3l1l1&dma=0&tag_exp=103116026~103200004~104527907~104528501~104684208~104684211~115495939~115616985~115938465~115938468~116185181~116185182~116988316~117041587&gclaw_src=0_1&npa=0&frm=0&gclgs=12&gclst=63271790&gcllp=265921241&gclaw=TestingABC-2026-01-28&pscdl=noapi&auid=712372246.1762244434&uaa=x86&uab=64&uafvl=Google%2520Chrome%3B143.0.7499.203%7CChromium%3B143.0.7499.203%7CNot%2520A(Brand%3B24.0.0.0&uamb=0&uam=&uap=Chrome%20OS&uapv=16463.79.0&uaw=0&ec_mode=c&gap.plf=4.3&ecsid=1296144687.1769663220&em=tv.1~em.h5JGBrQTGorO7q6IaFMfu5cSqqB6XTp1aybOD11spnQ~pn.QizoLG_BckrIeAQvfQVWU6temD0YbmFoJqctQ4S2ivg', method: 'GET' }, // form-data
263
+ { url: `https://www.googleadservices.com/ccm/conversion/11319954121/?random=1769663226870&cv=11&fst=1769663226870&fmt=3&bg=ffffff&guid=ON&async=1&en=conversion&gtm=45be61r1v9164572194za200zd9164572194xec&gcd=13l3l3l3l1l1&dma=0&tag_exp=103116026~103200004~104527907~104528501~104684208~104684211~115495939~115616985~115938465~115938468~116185181~116185182~116988316~117041587&u_w=1745&u_h=1090&url=https%3A%2F%2Fgrcn-sme.github.io%2Fwordpress-example%2F&ref=https%3A%2F%2Fgrcn-sme.github.io%2Fwordpress-example%2F&gclaw_src=2_3&label=yYk9CMf--98YEMmV45Uq&capi=1&gtm_ee=1&frm=0&tiba=Wordpress%20Example&value=1&currency_code=MYR&hn=www.googleadservices.com&npa=0&gclgs=12&gclst=63274872&gcllp=265921241&gclaw=TestingABC-2026-01-28&pscdl=noapi&auid=712372246.1762244434&uaa=x86&uab=64&uafvl=Google%2520Chrome%3B143.0.7499.203%7CChromium%3B143.0.7499.203%7CNot%2520A(Brand%3B24.0.0.0&uamb=0&uam=&uap=Chrome%20OS&uapv=16463.79.0&uaw=0&ec_mode=c&oid=113729285.1769663227&oidsrc=3&ecsid=1296144687.1769663220&_tu=ABA&gcl_ctr=399~0&data=event%3Dconversion&gap.plf=4.3&em=tv.1~em.h5JGBrQTGorO7q6IaFMfu5cSqqB6XTp1aybOD11spnQ~pn.QizoLG_BckrIeAQvfQVWU6temD0YbmFoJqctQ4S2ivg`, method: 'GET' } // form-data
264
+ ];
265
+
266
+ const sentRequests = [];
267
+ for (const req of sampleRequests) {
268
+ sentRequests.push(new SentRequest(req.url, req.method));
269
+ }
270
+ const validator = new EC4W_Validator(sentRequests);
271
+ const result = validator.conclude();
272
+
273
+ console.log({ result });
274
+ const invalid = result.invalidRequest;
275
+ console.log({ invalid });
276
+ console.assert(invalid === null, result);
277
+ // if (invalid) debugger;
278
+
279
+ } finally {
280
+ // console.groupEnd();
281
+ console.log('==========');
282
+ }
283
+ }
284
+
285
+ testValid('defualt');
286
+ testValid('good', [
287
+ new fakeUPDE('GET', null, '11319954121'),
288
+ new fakeUPDE('GET', null, '11319954121'),
289
+ new fakeConve('GET', defaultEMvalue, '11319954121', 'xxxxxxxxx'),
290
+ ]);
291
+
292
+ testValid('bad', [
293
+ new fakeUPDE('GET', 'tv.1', '11319954121'),
294
+ new fakeUPDE('GET', defaultEMvalue, '11319954121'),
295
+ new fakeConve('GET', 'tv.1', '11319954121', 'xxxxxxxxx'),
296
+ ]);
297
+
298
+ testValid('stitching', [
299
+ new fakeUPDE('GET', 'tv.1', '11319954121'),
300
+ new fakeUPDE('GET', defaultEMvalue, '11319954121'),
301
+ new fakeConve('GET', null, '11319954121', 'xxxxxxxxx'),
302
+ ]);
303
+
304
+ testValid('stitching: self empty em value', [
305
+ new fakeUPDE('GET', 'tv.1', '11319954121'),
306
+ new fakeUPDE('GET', 'defaultEMvalue', '11319954121'),
307
+ new fakeConve('GET', 'tv.1', '11319954121', 'xxxxxxxxx'),
308
+ ]);
309
+
310
+ testValid('stitching: invalid prior em', [
311
+ new fakeUPDE('GET', 'tv.1', '11319954121'),
312
+ new fakeUPDE('GET', defaultEMvalue, '11319954121'),
313
+ new fakeUPDE('GET', 'tv.1', '11319954121'),
314
+ new fakeConve('GET', null, '11319954121', 'xxxxxxxxx'),
315
+ ]);
316
+
317
+ console.log('end');
318
+ })();
@@ -0,0 +1,394 @@
1
+ export class EC4W_Validator {
2
+ /** @type {SentRequest[]} */
3
+ #requestList = [];
4
+
5
+ /** @param {SentRequest[]} requestList */
6
+ constructor(
7
+ requestList
8
+ ) {
9
+ this.#requestList = requestList;
10
+ }
11
+
12
+ /** @returns {EC4W_Result} */
13
+ conclude() {
14
+ // map // transform
15
+ const sentRequests = [];
16
+ for (const request of this.#requestList) {
17
+ if (!request.url) continue;
18
+
19
+ const Url = new URL(request.url);
20
+ if (UPDE_Request.canBe(request.method, Url)) {
21
+ sentRequests.push(new UPDE_Request(request.method, Url));
22
+ }
23
+ else if (Conversion_Request.canBe(request.method, Url)) {
24
+ sentRequests.push(new Conversion_Request(request.method, Url));
25
+ }
26
+ else {
27
+ // skip
28
+ }
29
+ }
30
+ if (sentRequests.length < 1) throw new Error('no ( sentRequest ) detected');
31
+
32
+
33
+
34
+
35
+ // groupping pairs
36
+ const EC4W_list = [];
37
+ {
38
+ let lastUPDE = null;
39
+ for (const ping of sentRequests) {
40
+ if (ping instanceof UPDE_Request) {
41
+ lastUPDE = ping;
42
+ }
43
+ else if (ping instanceof Conversion_Request) {
44
+ EC4W_list.push(new EC4W_Request(lastUPDE, ping));
45
+ }
46
+ }
47
+ }
48
+ if (EC4W_list.length < 1) throw new Error('no ( UPDE | EC4w ) requests detected');
49
+
50
+ return new EC4W_Result(EC4W_list);
51
+ }
52
+
53
+ /** @param {string} em */
54
+ static emIsValid(em) {
55
+ if (!em) return false;
56
+ if (em.startsWith('tv.1') === false) return false;
57
+ if (em === 'tv.1') return false;
58
+ if (/^tv\.1~e[0-9]/.test(em)) return false;
59
+ const part = em.split('~');
60
+ const len = part.length;
61
+ if (len < 2) return false;
62
+
63
+ for (let i = 1; i < len; ++i) {
64
+ if (part[i] === '') return false;
65
+ const [key, value] = part[i].split('.');
66
+ if (!key) return false;
67
+ if (!value) return false;
68
+ }
69
+
70
+ return true;
71
+ }
72
+
73
+ }
74
+
75
+
76
+
77
+ export const EC4W_ValidMessages = Object.freeze({
78
+ em_validInConversion: "valid `em` in Conversion payload",
79
+ em_validInUPDE: "valid `em` in UPDE payload",
80
+ });
81
+
82
+ export const EC4W_ErrorMessages = Object.freeze({
83
+ em_invalidInConversion: "invalid `em` in Conversion payload",
84
+ em_invalidInUPDE: "invalid `em` in UPDE payload",
85
+ ecsid_mismatch: "mismatch `ecsid` between UPDE and Conversion payload",
86
+ conversionId_mismatch: "mismatch `Conversion ID` between UPDE and Conversion payload",
87
+ noEC4W_request: "no valid EC4W requests detected at all",
88
+ });
89
+ export class EC4W_Request {
90
+ #UPDE_Request = null;
91
+ #Conversion_Request;
92
+
93
+ /**
94
+ * @param {UPDE_Request?} UPDE_Request
95
+ * @param {Conversion_Request} Conversion_Request */
96
+ constructor(
97
+ UPDE_Request,
98
+ Conversion_Request
99
+ ) {
100
+ this.#UPDE_Request = UPDE_Request;
101
+ this.#Conversion_Request = Conversion_Request;
102
+ }
103
+
104
+ get result() {
105
+ if (!this.#UPDE_Request && !this.#Conversion_Request)
106
+ return new Result(false, EC4W_ErrorMessages.noEC4W_request);
107
+ if (this.#ecsidIsValid() === false)
108
+ return new Result(false, EC4W_ErrorMessages.ecsid_mismatch);
109
+
110
+ return this.#emIsValid();
111
+ }
112
+
113
+ #ecsidIsValid() {
114
+ if (this.#Conversion_Request === null) return false;
115
+ if (this.#UPDE_Request === null) {
116
+ return true;
117
+ }
118
+
119
+ if (!this.#UPDE_Request.ecsid) return false;
120
+ if (!this.#Conversion_Request.ecsid) return false;
121
+ if (this.#UPDE_Request.ecsid !== this.#Conversion_Request.ecsid) return false;
122
+
123
+ return true;
124
+ }
125
+
126
+ #emIsValid() {
127
+ if (this.#Conversion_Request === null)
128
+ return new Result(false, EC4W_ErrorMessages.em_invalidInConversion);
129
+
130
+ if (this.#UPDE_Request === null) {
131
+ const isValid = this.#Conversion_Request.emIsValid();
132
+ if (isValid) return new Result(isValid, EC4W_ValidMessages.em_validInConversion);
133
+ else return new Result(isValid, EC4W_ErrorMessages.em_invalidInConversion);
134
+ }
135
+
136
+ if (this.#Conversion_Request.em === null) {
137
+ const isValid = this.#UPDE_Request.emIsValid();
138
+ if (isValid) return new Result(isValid, EC4W_ValidMessages.em_validInUPDE);
139
+ else return new Result(isValid, EC4W_ErrorMessages.em_invalidInUPDE);
140
+ }
141
+
142
+ const isValid = this.#Conversion_Request.emIsValid();
143
+ if (isValid) return new Result(isValid, EC4W_ValidMessages.em_validInConversion);
144
+ else return new Result(isValid, EC4W_ErrorMessages.em_invalidInConversion);
145
+ }
146
+ }
147
+
148
+ class Result {
149
+ #isGood;
150
+ get isGood() { return this.#isGood; }
151
+ #message;
152
+ get message() { return this.#message; }
153
+
154
+ /** @param {boolean} isGood
155
+ * @param {string} message */
156
+ constructor(isGood, message) {
157
+ this.#isGood = isGood;
158
+ this.#message = message;
159
+ }
160
+ }
161
+
162
+ export class UPDE_Request {
163
+ /**
164
+ * @param {string} method
165
+ * @param {URL} Url */
166
+ static canBe(method, Url) {
167
+ if (method !== 'GET') return false;
168
+ if (false === Url.toString().startsWith('https://www.google.com/ccm/form-data/')) return false;
169
+ if (Url.searchParams.get('em') === null) return false;
170
+ return true;
171
+ }
172
+
173
+ #method;
174
+ get method() { return this.#method; }
175
+ #Url;
176
+ get url() { return this.#Url.toString(); };
177
+
178
+ /**
179
+ * @param {string} method
180
+ * @param {URL} Url */
181
+ constructor(
182
+ method,
183
+ Url
184
+ ) {
185
+ this.#method = method;
186
+ this.#Url = Url;
187
+ }
188
+
189
+ get em() {
190
+ return this.#Url.searchParams.get('em');
191
+ }
192
+
193
+ get ecsid() {
194
+ return this.#Url.searchParams.get('ecsid');
195
+ }
196
+
197
+ emIsValid() {
198
+ return EC4W_Validator.emIsValid(this.em);
199
+ }
200
+
201
+ }
202
+
203
+ export class Conversion_Request {
204
+
205
+ /**
206
+ * @param {string} method
207
+ * @param {URL} Url */
208
+ static canBe(method, Url) {
209
+ if (method !== 'GET') return false;
210
+ if (false === Url.toString().startsWith('https://www.googleadservices.com/ccm/conversion/')) return false;
211
+ if (Url.searchParams.get('label') === null) return false;
212
+ return true;
213
+ }
214
+
215
+ #method;
216
+ get method() { return this.#method; }
217
+ #Url;
218
+ get url() { return this.#Url.toString(); };
219
+
220
+ /**
221
+ * @param {string} method
222
+ * @param {URL} Url */
223
+ constructor(
224
+ method,
225
+ Url
226
+ ) {
227
+ this.#method = method;
228
+ this.#Url = Url;
229
+ }
230
+
231
+ get em() {
232
+ return this.#Url.searchParams.get('em');
233
+ }
234
+
235
+ get ecsid() {
236
+ return this.#Url.searchParams.get('ecsid');
237
+ }
238
+
239
+ emIsValid() {
240
+ return EC4W_Validator.emIsValid(this.em);
241
+ }
242
+ }
243
+
244
+
245
+ export class EC4W_Result {
246
+ /** @type {EC4W_Request[]} */
247
+ EC4W_Requests = [];
248
+ // errors = [];
249
+ messages = [];
250
+
251
+ /**
252
+ * @param {EC4W_Request[]} EC4W_Requests
253
+ * */
254
+ constructor(
255
+ EC4W_Requests
256
+ ) {
257
+ if (!EC4W_Requests || EC4W_Requests.length < 1) throw new Error('no requests detected');
258
+ this.EC4W_Requests = EC4W_Requests;
259
+ }
260
+
261
+
262
+ get invalidRequest() {
263
+ for (const EC4W_Request of this.EC4W_Requests) {
264
+ if (EC4W_Request.result.isGood === false) return EC4W_Request;
265
+ }
266
+ return null;
267
+ }
268
+
269
+ get validRequest() {
270
+ for (const EC4W_Request of this.EC4W_Requests) {
271
+ if (EC4W_Request.result.isGood) return EC4W_Request;
272
+ }
273
+ return null;
274
+ }
275
+
276
+
277
+
278
+ // get summary() {
279
+ // const summary = [], EC4W_Requests = this.EC4W_Requests;
280
+ // for (const EC4W_Request of EC4W_Requests) {
281
+ // summary.push({
282
+ // EC4W_Request: EC4W_Request,
283
+ // result: EC4W_Request.result,
284
+ // });
285
+ // }
286
+ // return summary;
287
+ // }
288
+
289
+ }
290
+
291
+
292
+
293
+ export class SentRequest {
294
+ url = 'https://';
295
+ method = 'GET';
296
+
297
+ constructor(url, method) {
298
+ this.url = url;
299
+ this.method = method;
300
+ }
301
+ }
302
+
303
+
304
+
305
+
306
+ // (function testmain() { // test
307
+
308
+ // class fakeUPDE {
309
+ // constructor(httpMethod, emValue, conversionId) {
310
+ // this.conversionId = conversionId;
311
+ // this.emValue = emValue;
312
+
313
+ // const emParam = `&em=${emValue ? emValue : 'tv.1'}`;
314
+ // this.url = `https://www.google.com/ccm/form-data/${conversionId}?gtm=45be61r1p3v9164572194za200zd9164572194xec&gcd=13l3l3l3l1l1&dma=0&tag_exp=103116026~103200004~104527907~104528501~104684208~104684211~115495939~115616985~115938465~115938468~116185181~116185182~116988316~117041587&gclaw_src=0_1&npa=0&frm=0&gclgs=12&gclst=63271790&gcllp=265921241&gclaw=TestingABC-2026-01-28&pscdl=noapi&auid=712372246.1762244434&uaa=x86&uab=64&uafvl=Google%2520Chrome%3B143.0.7499.203%7CChromium%3B143.0.7499.203%7CNot%2520A(Brand%3B24.0.0.0&uamb=0&uam=&uap=Chrome%20OS&uapv=16463.79.0&uaw=0&ec_mode=c&gap.plf=4.3&ecsid=1296144687.1769663220${emParam}`
315
+ // this.method = httpMethod;
316
+ // }
317
+ // }
318
+
319
+ // class fakeConve {
320
+ // constructor(httpMethod, emValue, conversionId, conversionLabel) {
321
+ // this.emValue = emValue;
322
+ // this.conversionId = conversionId;
323
+ // this.conversionLabel = conversionLabel;
324
+
325
+ // const emParam = emValue ? `&em=${emValue}` : '';
326
+ // this.url = `https://www.googleadservices.com/ccm/conversion/${conversionId}/?random=1769663226870&cv=11&fst=1769663226870&fmt=3&bg=ffffff&guid=ON&async=1&en=conversion&gtm=45be61r1v9164572194za200zd9164572194xec&gcd=13l3l3l3l1l1&dma=0&tag_exp=103116026~103200004~104527907~104528501~104684208~104684211~115495939~115616985~115938465~115938468~116185181~116185182~116988316~117041587&u_w=1745&u_h=1090&url=https%3A%2F%2Fgrcn-sme.github.io%2Fwordpress-example%2F&ref=https%3A%2F%2Fgrcn-sme.github.io%2Fwordpress-example%2F&gclaw_src=2_3&label=yYk9CMf--98YEMmV45Uq&capi=1&gtm_ee=1&frm=0&tiba=Wordpress%20Example&value=1&currency_code=MYR&hn=www.googleadservices.com&npa=0&gclgs=12&gclst=63274872&gcllp=265921241&gclaw=TestingABC-2026-01-28&pscdl=noapi&auid=712372246.1762244434&uaa=x86&uab=64&uafvl=Google%2520Chrome%3B143.0.7499.203%7CChromium%3B143.0.7499.203%7CNot%2520A(Brand%3B24.0.0.0&uamb=0&uam=&uap=Chrome%20OS&uapv=16463.79.0&uaw=0&ec_mode=c&oid=113729285.1769663227&oidsrc=3&ecsid=1296144687.1769663220&_tu=ABA&gcl_ctr=399~0&data=event%3Dconversion&gap.plf=4.3${emParam}`;
327
+ // this.method = httpMethod;
328
+ // }
329
+ // }
330
+ // const defaultEMvalue = 'tv.1~em.h5JGBrQTGorO7q6IaFMfu5cSqqB6XTp1aybOD11spnQ~pn.QizoLG_BckrIeAQvfQVWU6temD0YbmFoJqctQ4S2ivg';
331
+
332
+
333
+ // function testValid(testName, inputRequests) {
334
+ // console.log('==========');
335
+ // console.log(testName);
336
+ // try {
337
+ // const sampleRequests = inputRequests || [
338
+ // { url: 'https://www.google.com/ccm/form-data/11319954121?gtm=45be61r1p3v9164572194za200zd9164572194xec&gcd=13l3l3l3l1l1&dma=0&tag_exp=103116026~103200004~104527907~104528501~104684208~104684211~115495939~115616985~115938465~115938468~116185181~116185182~116988316~117041587&gclaw_src=0_1&npa=0&frm=0&gclgs=12&gclst=63271790&gcllp=265921241&gclaw=TestingABC-2026-01-28&pscdl=noapi&auid=712372246.1762244434&uaa=x86&uab=64&uafvl=Google%2520Chrome%3B143.0.7499.203%7CChromium%3B143.0.7499.203%7CNot%2520A(Brand%3B24.0.0.0&uamb=0&uam=&uap=Chrome%20OS&uapv=16463.79.0&uaw=0&ec_mode=c&gap.plf=4.3&ecsid=1296144687.1769663220&em=tv.1~em.h5JGBrQTGorO7q6IaFMfu5cSqqB6XTp1aybOD11spnQ~pn.QizoLG_BckrIeAQvfQVWU6temD0YbmFoJqctQ4S2ivg', method: 'GET' }, // form-data
339
+ // { url: `https://www.googleadservices.com/ccm/conversion/11319954121/?random=1769663226870&cv=11&fst=1769663226870&fmt=3&bg=ffffff&guid=ON&async=1&en=conversion&gtm=45be61r1v9164572194za200zd9164572194xec&gcd=13l3l3l3l1l1&dma=0&tag_exp=103116026~103200004~104527907~104528501~104684208~104684211~115495939~115616985~115938465~115938468~116185181~116185182~116988316~117041587&u_w=1745&u_h=1090&url=https%3A%2F%2Fgrcn-sme.github.io%2Fwordpress-example%2F&ref=https%3A%2F%2Fgrcn-sme.github.io%2Fwordpress-example%2F&gclaw_src=2_3&label=yYk9CMf--98YEMmV45Uq&capi=1&gtm_ee=1&frm=0&tiba=Wordpress%20Example&value=1&currency_code=MYR&hn=www.googleadservices.com&npa=0&gclgs=12&gclst=63274872&gcllp=265921241&gclaw=TestingABC-2026-01-28&pscdl=noapi&auid=712372246.1762244434&uaa=x86&uab=64&uafvl=Google%2520Chrome%3B143.0.7499.203%7CChromium%3B143.0.7499.203%7CNot%2520A(Brand%3B24.0.0.0&uamb=0&uam=&uap=Chrome%20OS&uapv=16463.79.0&uaw=0&ec_mode=c&oid=113729285.1769663227&oidsrc=3&ecsid=1296144687.1769663220&_tu=ABA&gcl_ctr=399~0&data=event%3Dconversion&gap.plf=4.3&em=tv.1~em.h5JGBrQTGorO7q6IaFMfu5cSqqB6XTp1aybOD11spnQ~pn.QizoLG_BckrIeAQvfQVWU6temD0YbmFoJqctQ4S2ivg`, method: 'GET' } // form-data
340
+ // ];
341
+
342
+ // const sentRequests = [];
343
+ // for (const req of sampleRequests) {
344
+ // sentRequests.push(new SentRequest(req.url, req.method));
345
+ // }
346
+ // const validator = new EC4W_Validator(sentRequests);
347
+ // const result = validator.conclude();
348
+
349
+ // console.log({ result });
350
+ // const invalid = result.invalidRequest;
351
+ // console.log({ invalid });
352
+ // console.assert(invalid === null, result);
353
+ // // if (invalid) debugger;
354
+
355
+ // } finally {
356
+ // // console.groupEnd();
357
+ // console.log('==========');
358
+ // }
359
+ // }
360
+
361
+ // testValid('defualt');
362
+ // testValid('good', [
363
+ // new fakeUPDE('GET', null, '11319954121'),
364
+ // new fakeUPDE('GET', null, '11319954121'),
365
+ // new fakeConve('GET', defaultEMvalue, '11319954121', 'xxxxxxxxx'),
366
+ // ]);
367
+
368
+ // testValid('bad', [
369
+ // new fakeUPDE('GET', 'tv.1', '11319954121'),
370
+ // new fakeUPDE('GET', defaultEMvalue, '11319954121'),
371
+ // new fakeConve('GET', 'tv.1', '11319954121', 'xxxxxxxxx'),
372
+ // ]);
373
+
374
+ // testValid('stitching', [
375
+ // new fakeUPDE('GET', 'tv.1', '11319954121'),
376
+ // new fakeUPDE('GET', defaultEMvalue, '11319954121'),
377
+ // new fakeConve('GET', null, '11319954121', 'xxxxxxxxx'),
378
+ // ]);
379
+
380
+ // testValid('stitching: self empty em value', [
381
+ // new fakeUPDE('GET', 'tv.1', '11319954121'),
382
+ // new fakeUPDE('GET', 'defaultEMvalue', '11319954121'),
383
+ // new fakeConve('GET', 'tv.1', '11319954121', 'xxxxxxxxx'),
384
+ // ]);
385
+
386
+ // testValid('stitching: invalid prior em', [
387
+ // new fakeUPDE('GET', 'tv.1', '11319954121'),
388
+ // new fakeUPDE('GET', defaultEMvalue, '11319954121'),
389
+ // new fakeUPDE('GET', 'tv.1', '11319954121'),
390
+ // new fakeConve('GET', null, '11319954121', 'xxxxxxxxx'),
391
+ // ]);
392
+
393
+ // console.log('end');
394
+ // });
@@ -0,0 +1,151 @@
1
+ import { describe, it, test, expect } from 'vitest';
2
+ import { EC4W_Validator } from "/src/ec4w_validator.js";
3
+ import { EC4W_ValidMessages, EC4W_ErrorMessages } from '/src/ec4w_validator.js';
4
+
5
+ class fakeUPDE {
6
+ constructor({ httpMethod, em, ecsid = null, conversionId }) {
7
+ const updeLink = new URL(`https://www.google.com/ccm/form-data/${conversionId}?gtm=45be61r1p3v9164572194za200zd9164572194xec&gcd=13l3l3l3l1l1&dma=0&tag_exp=103116026~103200004~104527907~104528501~104684208~104684211~115495939~115616985~115938465~115938468~116185181~116185182~116988316~117041587&gclaw_src=0_1&npa=0&frm=0&gclgs=12&gclst=63271790&gcllp=265921241&gclaw=TestingABC-2026-01-28&pscdl=noapi&auid=712372246.1762244434&uaa=x86&uab=64&uafvl=Google%2520Chrome%3B143.0.7499.203%7CChromium%3B143.0.7499.203%7CNot%2520A(Brand%3B24.0.0.0&uamb=0&uam=&uap=Chrome%20OS&uapv=16463.79.0&uaw=0&ec_mode=c&gap.plf=4.3&ecsid=1296144687.1769663220&em=tv.1`);
8
+ const params = updeLink.searchParams;
9
+ params.set('em', em || 'tv.1');
10
+ params.set('ecsid', ecsid ?? '1296144687.1769663220');
11
+
12
+ this.url = updeLink.toString();
13
+ this.method = httpMethod;
14
+ }
15
+ }
16
+
17
+ class fakeConve {
18
+ constructor({ httpMethod, em, ecsid = null, conversionId, conversionLabel }) {
19
+ const convLink = new URL(`https://www.googleadservices.com/ccm/conversion/${conversionId}/?random=1769663226870&cv=11&fst=1769663226870&fmt=3&bg=ffffff&guid=ON&async=1&en=conversion&gtm=45be61r1v9164572194za200zd9164572194xec&gcd=13l3l3l3l1l1&dma=0&tag_exp=103116026~103200004~104527907~104528501~104684208~104684211~115495939~115616985~115938465~115938468~116185181~116185182~116988316~117041587&u_w=1745&u_h=1090&url=https%3A%2F%2Fgrcn-sme.github.io%2Fwordpress-example%2F&ref=https%3A%2F%2Fgrcn-sme.github.io%2Fwordpress-example%2F&gclaw_src=2_3&label=yYk9CMf--98YEMmV45Uq&capi=1&gtm_ee=1&frm=0&tiba=Wordpress%20Example&value=1&currency_code=MYR&hn=www.googleadservices.com&npa=0&gclgs=12&gclst=63274872&gcllp=265921241&gclaw=TestingABC-2026-01-28&pscdl=noapi&auid=712372246.1762244434&uaa=x86&uab=64&uafvl=Google%2520Chrome%3B143.0.7499.203%7CChromium%3B143.0.7499.203%7CNot%2520A(Brand%3B24.0.0.0&uamb=0&uam=&uap=Chrome%20OS&uapv=16463.79.0&uaw=0&ec_mode=c&oid=113729285.1769663227&oidsrc=3&ecsid=1296144687.1769663220&_tu=ABA&gcl_ctr=399~0&data=event%3Dconversion&gap.plf=4.3&em=tv.1`);
20
+ const params = convLink.searchParams;
21
+ if (em !== null) params.set('em', em);
22
+ else params.delete('em');
23
+ params.set('ecsid', ecsid ?? '1296144687.1769663220');
24
+ params.set('label', conversionLabel);
25
+
26
+ this.url = convLink.toString();
27
+ this.method = httpMethod;
28
+ }
29
+ }
30
+ const defaultEMvalue = 'tv.1~em.h5JGBrQTGorO7q6IaFMfu5cSqqB6XTp1aybOD11spnQ~pn.QizoLG_BckrIeAQvfQVWU6temD0YbmFoJqctQ4S2ivg';
31
+
32
+
33
+
34
+ test('good Conversion em', function () {
35
+ const sample = [
36
+ new fakeUPDE({ httpMethod: 'GET', em: null, conversionId: '11319954121' }),
37
+ new fakeUPDE({ httpMethod: 'GET', em: null, conversionId: '11319954121' }),
38
+ new fakeConve({ httpMethod: 'GET', em: defaultEMvalue, conversionId: '11319954121', conversionLabel: 'xxoos_s_ssw' }),
39
+ ];
40
+ const EC4W_validator = new EC4W_Validator(sample);
41
+ const result = EC4W_validator.conclude();
42
+
43
+ expect(result.invalidRequest).toBeNull();
44
+ });
45
+
46
+ test('bad conversion em', function () {
47
+
48
+ const sample = [
49
+ new fakeUPDE({ httpMethod: 'GET', em: 'tv.1', conversionId: '11319954121' }),
50
+ new fakeUPDE({ httpMethod: 'GET', em: defaultEMvalue, conversionId: '11319954121' }),
51
+ new fakeConve({ httpMethod: 'GET', em: 'tv.1', conversionId: '11319954121', conversionLabel: 'xx_x_xxxxxx' }),
52
+ ];
53
+ const EC4W_validator = new EC4W_Validator(sample);
54
+ const result = EC4W_validator.conclude();
55
+
56
+ expect(result.invalidRequest).toBeTruthy();
57
+ const re = result.invalidRequest.result;
58
+ expect(re.isGood).toBe(false);
59
+ expect(re.message).toBe(EC4W_ErrorMessages.em_invalidInConversion);
60
+
61
+
62
+ });
63
+
64
+ test('stitching: proper', function () {
65
+
66
+ const sample = [
67
+ new fakeUPDE({ httpMethod: 'GET', em: 'tv.1', conversionId: '11319954121' }),
68
+ new fakeUPDE({ httpMethod: 'GET', em: defaultEMvalue, conversionId: '11319954121' }),
69
+ new fakeConve({ httpMethod: 'GET', em: null, conversionId: '11319954121', conversionLabel: 'xxxxxxxxx' }),
70
+ ];
71
+ const EC4W_validator = new EC4W_Validator(sample);
72
+ const result = EC4W_validator.conclude();
73
+
74
+ expect(result.invalidRequest).toBeNull();
75
+
76
+ const EC4W_Request = result.validRequest;
77
+ expect(EC4W_Request).toBeTruthy();
78
+ const re = EC4W_Request.result;
79
+ expect(re.isGood).toBe(true);
80
+ expect(re.message).toBe(EC4W_ValidMessages.em_validInUPDE);
81
+ expect(re.message).not.toBe(EC4W_ValidMessages.em_validInConversion);
82
+ });
83
+
84
+ test('stitching: self empty em value', function () {
85
+ const sample = [
86
+ new fakeUPDE({ httpMethod: 'GET', em: 'tv.1', conversionId: '11319954121' }),
87
+ new fakeUPDE({ httpMethod: 'GET', em: defaultEMvalue, conversionId: '11319954121' }),
88
+ new fakeConve({ httpMethod: 'GET', em: 'tv.1', conversionId: '11319954121', conversionLabel: 'xxxxxxxxx' }),
89
+ ];
90
+
91
+ const EC4W_validator = new EC4W_Validator(sample);
92
+ const result = EC4W_validator.conclude();
93
+
94
+
95
+ expect(result.invalidRequest).toBeTruthy();
96
+
97
+ const re = result.invalidRequest.result;
98
+ expect(re.isGood).toBe(false);
99
+ expect(re.message).toBe(EC4W_ErrorMessages.em_invalidInConversion);
100
+ expect(re.message).not.toBe(EC4W_ValidMessages.em_validInUPDE);
101
+
102
+
103
+ });
104
+
105
+
106
+
107
+ test('stitching: invalid prior em', function () {
108
+
109
+ const sample = [
110
+ new fakeUPDE({ httpMethod: 'GET', em: 'tv.1', conversionId: '11319954121' }),
111
+ new fakeUPDE({ httpMethod: 'GET', em: defaultEMvalue, conversionId: '11319954121' }),
112
+ new fakeUPDE({ httpMethod: 'GET', em: 'tv.1', conversionId: '11319954121' }),
113
+ new fakeConve({ httpMethod: 'GET', em: null, conversionId: '11319954121', conversionLabel: 'xxxxxxxxx' }),
114
+ ];
115
+ const EC4W_validator = new EC4W_Validator(sample);
116
+ const result = EC4W_validator.conclude();
117
+
118
+
119
+ expect(result.invalidRequest).toBeTruthy();
120
+
121
+ const re = result.invalidRequest.result;
122
+ expect(re.isGood).toBe(false);
123
+ expect(re.message).toBe(EC4W_ErrorMessages.em_invalidInUPDE);
124
+ expect(re.message).not.toBe(EC4W_ErrorMessages.em_invalidInConversion);
125
+
126
+ });
127
+
128
+
129
+
130
+
131
+
132
+
133
+ test('stitching: different ecsid', function () {
134
+
135
+ const sample = [
136
+ new fakeUPDE({ httpMethod: 'GET', em: 'tv.1', ecsid: '1120.0300', conversionId: '11319954121' }),
137
+ new fakeUPDE({ httpMethod: 'GET', em: 'defaultEMvalue', ecsid: '1120.0300', conversionId: '11319954121' }),
138
+ new fakeConve({ httpMethod: 'GET', em: null, ecsid: '9920.22', conversionId: '11319954121', conversionLabel: 'xxxxxxxxx' }),
139
+ ];
140
+ const EC4W_validator = new EC4W_Validator(sample);
141
+ const result = EC4W_validator.conclude();
142
+
143
+
144
+ expect(result.invalidRequest).toBeTruthy();
145
+
146
+ const re = result.invalidRequest.result;
147
+ expect(re.isGood).toBe(false);
148
+ expect(re.message).toBe(EC4W_ErrorMessages.ecsid_mismatch);
149
+
150
+
151
+ });
package/src/main.js ADDED
@@ -0,0 +1 @@
1
+ // import {*} from './ec4w_validator.js';
package/vite.config.ts ADDED
@@ -0,0 +1,20 @@
1
+ import { defineConfig } from 'vite';
2
+ // import dts from 'vite-plugin-dts';
3
+
4
+ export default defineConfig({
5
+ // plugins: [dts({ rollupTypes: true })], // Bundles types into one file
6
+ build: {
7
+ lib: {
8
+ entry: 'src/ec4w_validator.js',
9
+ name: 'MyLib', // The global variable used in UMD builds
10
+ fileName: (format) => `my-lib.${format}.js`
11
+ },
12
+ rollupOptions: {
13
+ // Don't bundle these into the library
14
+ external: ['vue'],
15
+ output: {
16
+ globals: { vue: 'Vue' }
17
+ }
18
+ }
19
+ }
20
+ })
@@ -0,0 +1,12 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ // Allows you to use 'describe', 'it', and 'expect' without importing them
6
+ globals: true,
7
+ // Simulates a browser environment (important for React/Vue)
8
+ environment: 'jsdom',
9
+ // Path to your test files
10
+ include: ['**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
11
+ },
12
+ });