@el7ven/cookie-kit 0.2.21 → 0.3.2
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/package.json +21 -1
- package/src/core/AnalyticsManager.js +400 -0
- package/src/core/ConsentManager.js +710 -0
- package/src/core/ConsentMode.js +109 -0
- package/src/core/FocusTrap.js +130 -0
- package/src/core/GeoDetector.js +144 -0
- package/src/core/ScriptLoader.js +229 -0
- package/src/core/StorageAdapter.js +179 -0
- package/src/core/analytics.js +101 -10
- package/src/core/index.js +7 -7
- package/src/geo/GeoDetector.js +536 -0
- package/src/geo/RegionRules.js +126 -0
- package/src/geo/index.js +16 -0
- package/src/index.js +56 -18
- package/src/js/CookieConsent.js +0 -1
- package/src/locales/en.js +54 -0
- package/src/locales/index.js +20 -0
- package/src/locales/ro.js +54 -0
- package/src/plugins/CMPPlugin.js +187 -0
- package/src/plugins/PluginManager.js +234 -0
- package/src/plugins/index.js +7 -0
- package/src/providers/GoogleConsentModeProvider.js +278 -0
- package/src/providers/index.js +6 -0
- package/src/rewriting/ScriptRewriter.js +278 -0
- package/src/rewriting/index.js +6 -0
- package/src/scripts/ScriptLoader.js +310 -0
- package/src/scripts/ScriptManager.js +278 -0
- package/src/scripts/ScriptRegistry.js +175 -0
- package/src/scripts/ScriptScanner.js +178 -0
- package/src/scripts/index.js +9 -0
- package/src/trackers/TrackerDetector.js +488 -0
- package/src/trackers/TrackerPatterns.js +307 -0
- package/src/trackers/TrackerRegistry.js +172 -0
- package/src/trackers/index.js +15 -0
- package/src/utils/cookies.js +37 -0
- package/src/utils/dom.js +58 -0
- package/src/utils/helpers.js +89 -0
- package/src/vue/CookieConsent.vue +1 -1
- package/src/vue/CookieDrawer.vue +4 -4
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google Consent Mode v2 Provider
|
|
3
|
+
* Required for Google Ads, GA4, and GTM compliance
|
|
4
|
+
* @module GoogleConsentModeProvider
|
|
5
|
+
* @see https://developers.google.com/tag-platform/security/guides/consent
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export class GoogleConsentModeProvider {
|
|
9
|
+
constructor(consentManager, config = {}) {
|
|
10
|
+
this.consentManager = consentManager
|
|
11
|
+
this.config = {
|
|
12
|
+
// Category mapping: CMP category → Google consent type
|
|
13
|
+
categoryMapping: {
|
|
14
|
+
analytics: ['analytics_storage'],
|
|
15
|
+
marketing: ['ad_storage', 'ad_user_data', 'ad_personalization'],
|
|
16
|
+
preferences: ['functionality_storage', 'personalization_storage'],
|
|
17
|
+
...config.categoryMapping
|
|
18
|
+
},
|
|
19
|
+
// Wait for GTM/GA to load
|
|
20
|
+
waitForGtag: true,
|
|
21
|
+
// Region-specific settings
|
|
22
|
+
regionSettings: {
|
|
23
|
+
EU: { wait_for_update: 500 },
|
|
24
|
+
UK: { wait_for_update: 500 },
|
|
25
|
+
US: { wait_for_update: 0 },
|
|
26
|
+
...config.regionSettings
|
|
27
|
+
},
|
|
28
|
+
// Enable URL passthrough for conversion tracking
|
|
29
|
+
urlPassthrough: true,
|
|
30
|
+
// Enable ads data redaction
|
|
31
|
+
adsDataRedaction: true,
|
|
32
|
+
...config
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
this.isInitialized = false
|
|
36
|
+
this.lastConsentHash = null // Prevent duplicate updates
|
|
37
|
+
this._debug('GoogleConsentModeProvider created')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Initialize Google Consent Mode
|
|
42
|
+
* MUST be called before GA/GTM loads
|
|
43
|
+
*/
|
|
44
|
+
initialize() {
|
|
45
|
+
if (this.isInitialized) {
|
|
46
|
+
this._debug('Already initialized')
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Bootstrap gtag (CRITICAL: before GA/GTM loads)
|
|
51
|
+
this._bootstrapGtag()
|
|
52
|
+
|
|
53
|
+
// Set default consent state (DENIED for all)
|
|
54
|
+
this.setDefaultConsent()
|
|
55
|
+
|
|
56
|
+
// Listen for consent changes
|
|
57
|
+
this.consentManager.on('consentChanged', ({ consent, categories }) => {
|
|
58
|
+
this.updateConsent(categories, consent.region)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
// Apply existing consent if available
|
|
62
|
+
const existingConsent = this.consentManager.getConsent()
|
|
63
|
+
if (existingConsent) {
|
|
64
|
+
this.updateConsent(existingConsent.categories, existingConsent.region)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
this.isInitialized = true
|
|
68
|
+
this._debug('Google Consent Mode v2 initialized')
|
|
69
|
+
this._emitEvent('gcm:initialized', {})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Set default consent state (all DENIED)
|
|
74
|
+
* Called before GA/GTM loads
|
|
75
|
+
*/
|
|
76
|
+
setDefaultConsent() {
|
|
77
|
+
const region = this.consentManager.getConsent()?.region || 'ROW'
|
|
78
|
+
const regionConfig = this.config.regionSettings[region] || {}
|
|
79
|
+
|
|
80
|
+
const defaultConsent = {
|
|
81
|
+
ad_storage: 'denied',
|
|
82
|
+
analytics_storage: 'denied',
|
|
83
|
+
ad_user_data: 'denied',
|
|
84
|
+
ad_personalization: 'denied',
|
|
85
|
+
functionality_storage: 'denied',
|
|
86
|
+
personalization_storage: 'denied',
|
|
87
|
+
security_storage: 'granted', // Always granted for security
|
|
88
|
+
|
|
89
|
+
// CRITICAL: wait_for_update gives CMP time to update consent
|
|
90
|
+
// Prevents GA from firing before consent update
|
|
91
|
+
wait_for_update: regionConfig.wait_for_update !== undefined
|
|
92
|
+
? regionConfig.wait_for_update
|
|
93
|
+
: 500, // Default 500ms for all regions
|
|
94
|
+
|
|
95
|
+
...regionConfig
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// URL passthrough for conversion tracking without cookies
|
|
99
|
+
if (this.config.urlPassthrough) {
|
|
100
|
+
defaultConsent.url_passthrough = true
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Redact ads data until consent
|
|
104
|
+
if (this.config.adsDataRedaction) {
|
|
105
|
+
defaultConsent.ads_data_redaction = true
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
this._debug('Setting default consent:', defaultConsent)
|
|
109
|
+
|
|
110
|
+
window.gtag('consent', 'default', defaultConsent)
|
|
111
|
+
|
|
112
|
+
this._emitEvent('gcm:default-set', { consent: defaultConsent })
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Update consent based on user choice
|
|
117
|
+
* @param {Object} categories - Consent categories
|
|
118
|
+
* @param {string} region - User region
|
|
119
|
+
*/
|
|
120
|
+
updateConsent(categories, region = 'ROW') {
|
|
121
|
+
const consentUpdate = this._mapCategoriesToGoogleConsent(categories)
|
|
122
|
+
|
|
123
|
+
// Deduplication: prevent duplicate updates
|
|
124
|
+
const consentHash = this._hashConsent(consentUpdate)
|
|
125
|
+
if (this.lastConsentHash === consentHash) {
|
|
126
|
+
this._debug('Consent unchanged, skipping update')
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
this.lastConsentHash = consentHash
|
|
130
|
+
|
|
131
|
+
this._debug('Updating consent:', consentUpdate)
|
|
132
|
+
|
|
133
|
+
window.gtag('consent', 'update', consentUpdate)
|
|
134
|
+
|
|
135
|
+
this._emitEvent('gcm:updated', {
|
|
136
|
+
consent: consentUpdate,
|
|
137
|
+
categories,
|
|
138
|
+
region
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Map CMP categories to Google consent types
|
|
144
|
+
* @param {Object} categories - CMP categories
|
|
145
|
+
* @returns {Object} Google consent object
|
|
146
|
+
* @private
|
|
147
|
+
*/
|
|
148
|
+
_mapCategoriesToGoogleConsent(categories) {
|
|
149
|
+
const googleConsent = {}
|
|
150
|
+
|
|
151
|
+
// Map each CMP category to Google consent types
|
|
152
|
+
Object.entries(this.config.categoryMapping).forEach(([cmpCategory, googleTypes]) => {
|
|
153
|
+
const isGranted = categories[cmpCategory] === true
|
|
154
|
+
const consentValue = isGranted ? 'granted' : 'denied'
|
|
155
|
+
|
|
156
|
+
googleTypes.forEach(googleType => {
|
|
157
|
+
googleConsent[googleType] = consentValue
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
// Security storage always granted
|
|
162
|
+
googleConsent.security_storage = 'granted'
|
|
163
|
+
|
|
164
|
+
return googleConsent
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Get current consent state
|
|
169
|
+
* @returns {Object}
|
|
170
|
+
*/
|
|
171
|
+
getConsentState() {
|
|
172
|
+
const consent = this.consentManager.getConsent()
|
|
173
|
+
if (!consent) {
|
|
174
|
+
return {
|
|
175
|
+
ad_storage: 'denied',
|
|
176
|
+
analytics_storage: 'denied',
|
|
177
|
+
ad_user_data: 'denied',
|
|
178
|
+
ad_personalization: 'denied'
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return this._mapCategoriesToGoogleConsent(consent.categories)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Check if specific Google consent type is granted
|
|
187
|
+
* @param {string} consentType - Google consent type
|
|
188
|
+
* @returns {boolean}
|
|
189
|
+
*/
|
|
190
|
+
isGranted(consentType) {
|
|
191
|
+
const state = this.getConsentState()
|
|
192
|
+
return state[consentType] === 'granted'
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Manually trigger consent update
|
|
197
|
+
* Useful for testing or manual control
|
|
198
|
+
*/
|
|
199
|
+
refresh() {
|
|
200
|
+
const consent = this.consentManager.getConsent()
|
|
201
|
+
if (consent) {
|
|
202
|
+
this.updateConsent(consent.categories, consent.region)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Get category mapping configuration
|
|
208
|
+
* @returns {Object}
|
|
209
|
+
*/
|
|
210
|
+
getCategoryMapping() {
|
|
211
|
+
return { ...this.config.categoryMapping }
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Update category mapping
|
|
216
|
+
* @param {Object} mapping - New mapping
|
|
217
|
+
*/
|
|
218
|
+
setCategoryMapping(mapping) {
|
|
219
|
+
this.config.categoryMapping = { ...this.config.categoryMapping, ...mapping }
|
|
220
|
+
this.refresh()
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// --- Internal helpers ---
|
|
224
|
+
|
|
225
|
+
_emitEvent(eventName, detail) {
|
|
226
|
+
if (typeof window !== 'undefined') {
|
|
227
|
+
window.dispatchEvent(new CustomEvent(`cookie-consent:${eventName}`, { detail }))
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Bootstrap gtag function
|
|
233
|
+
* MUST run before GA/GTM loads
|
|
234
|
+
* @private
|
|
235
|
+
*/
|
|
236
|
+
_bootstrapGtag() {
|
|
237
|
+
// Initialize dataLayer
|
|
238
|
+
window.dataLayer = window.dataLayer || []
|
|
239
|
+
|
|
240
|
+
// Create gtag function if not exists
|
|
241
|
+
if (!window.gtag) {
|
|
242
|
+
window.gtag = function() {
|
|
243
|
+
window.dataLayer.push(arguments)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Mark gtag as bootstrapped by CMP
|
|
247
|
+
window.gtag.l = Date.now()
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
this._debug('gtag bootstrapped')
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Hash consent object for deduplication
|
|
255
|
+
* @private
|
|
256
|
+
*/
|
|
257
|
+
_hashConsent(consent) {
|
|
258
|
+
return JSON.stringify(consent)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
_debug(...args) {
|
|
262
|
+
if (this.consentManager.config.debug) {
|
|
263
|
+
console.log('[GoogleConsentMode]', ...args)
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Helper function to initialize Google Consent Mode
|
|
270
|
+
* @param {ConsentManager} consentManager - Consent manager instance
|
|
271
|
+
* @param {Object} config - Configuration
|
|
272
|
+
* @returns {GoogleConsentModeProvider}
|
|
273
|
+
*/
|
|
274
|
+
export function initGoogleConsentMode(consentManager, config = {}) {
|
|
275
|
+
const provider = new GoogleConsentModeProvider(consentManager, config)
|
|
276
|
+
provider.initialize()
|
|
277
|
+
return provider
|
|
278
|
+
}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ScriptRewriter — automatically rewrites scripts to be CMP-compliant
|
|
3
|
+
* @module ScriptRewriter
|
|
4
|
+
*
|
|
5
|
+
* Converts regular scripts:
|
|
6
|
+
* <script src="https://www.google-analytics.com/gtag.js"></script>
|
|
7
|
+
*
|
|
8
|
+
* Into CMP-compliant scripts:
|
|
9
|
+
* <script
|
|
10
|
+
* type="text/plain"
|
|
11
|
+
* data-cookie-category="analytics"
|
|
12
|
+
* data-cookie-src="https://www.google-analytics.com/gtag.js"
|
|
13
|
+
* ></script>
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { getAllTrackerPatterns } from '../trackers/TrackerPatterns.js'
|
|
17
|
+
|
|
18
|
+
export class ScriptRewriter {
|
|
19
|
+
constructor(config = {}) {
|
|
20
|
+
this.config = {
|
|
21
|
+
autoRewrite: true,
|
|
22
|
+
rewriteInline: false, // Rewrite inline scripts (dangerous!)
|
|
23
|
+
preserveAttributes: true, // Preserve original attributes
|
|
24
|
+
observeDOM: true, // Watch for new scripts
|
|
25
|
+
whitelist: [], // URLs to never rewrite
|
|
26
|
+
blacklist: [], // URLs to always rewrite
|
|
27
|
+
debug: false,
|
|
28
|
+
...config
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
this.rewrittenScripts = new Map() // original script -> rewritten script
|
|
32
|
+
this.observer = null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Initialize rewriter
|
|
37
|
+
*/
|
|
38
|
+
initialize() {
|
|
39
|
+
if (this.config.autoRewrite) {
|
|
40
|
+
this.rewriteAll()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (this.config.observeDOM) {
|
|
44
|
+
this._startObserver()
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Rewrite all scripts on the page
|
|
50
|
+
* @returns {number} Number of scripts rewritten
|
|
51
|
+
*/
|
|
52
|
+
rewriteAll() {
|
|
53
|
+
this._debug('Starting script rewriting...')
|
|
54
|
+
|
|
55
|
+
const scripts = document.querySelectorAll('script[src]')
|
|
56
|
+
let rewrittenCount = 0
|
|
57
|
+
|
|
58
|
+
scripts.forEach(script => {
|
|
59
|
+
if (this._shouldRewrite(script)) {
|
|
60
|
+
const rewritten = this._rewriteScript(script)
|
|
61
|
+
if (rewritten) {
|
|
62
|
+
rewrittenCount++
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
this._debug(`Rewrote ${rewrittenCount} scripts`)
|
|
68
|
+
return rewrittenCount
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Rewrite a single script element
|
|
73
|
+
* @param {HTMLScriptElement} script - Script to rewrite
|
|
74
|
+
* @returns {HTMLScriptElement|null} Rewritten script or null
|
|
75
|
+
*/
|
|
76
|
+
_rewriteScript(script) {
|
|
77
|
+
const src = script.getAttribute('src')
|
|
78
|
+
if (!src) return null
|
|
79
|
+
|
|
80
|
+
// Check if already rewritten
|
|
81
|
+
if (script.type === 'text/plain' && script.dataset.cookieSrc) {
|
|
82
|
+
this._debug('Script already rewritten:', src)
|
|
83
|
+
return null
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Detect tracker category
|
|
87
|
+
const category = this._detectCategory(src)
|
|
88
|
+
if (!category) {
|
|
89
|
+
this._debug('No category detected for:', src)
|
|
90
|
+
return null
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Create new script element
|
|
94
|
+
const newScript = document.createElement('script')
|
|
95
|
+
|
|
96
|
+
// Set CMP attributes
|
|
97
|
+
newScript.type = 'text/plain'
|
|
98
|
+
newScript.dataset.cookieCategory = category
|
|
99
|
+
newScript.dataset.cookieSrc = src
|
|
100
|
+
|
|
101
|
+
// Preserve original attributes
|
|
102
|
+
if (this.config.preserveAttributes) {
|
|
103
|
+
Array.from(script.attributes).forEach(attr => {
|
|
104
|
+
if (attr.name !== 'src' && attr.name !== 'type') {
|
|
105
|
+
newScript.setAttribute(attr.name, attr.value)
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Replace script
|
|
111
|
+
script.parentNode?.replaceChild(newScript, script)
|
|
112
|
+
|
|
113
|
+
// Store mapping
|
|
114
|
+
this.rewrittenScripts.set(script, newScript)
|
|
115
|
+
|
|
116
|
+
this._debug(`Rewrote script: ${src} → category: ${category}`)
|
|
117
|
+
return newScript
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Check if script should be rewritten
|
|
122
|
+
* @param {HTMLScriptElement} script
|
|
123
|
+
* @returns {boolean}
|
|
124
|
+
*/
|
|
125
|
+
_shouldRewrite(script) {
|
|
126
|
+
const src = script.getAttribute('src')
|
|
127
|
+
if (!src) return false
|
|
128
|
+
|
|
129
|
+
// Already rewritten
|
|
130
|
+
if (script.type === 'text/plain' && script.dataset.cookieSrc) {
|
|
131
|
+
return false
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Check whitelist (never rewrite)
|
|
135
|
+
if (this.config.whitelist.length > 0) {
|
|
136
|
+
const isWhitelisted = this.config.whitelist.some(pattern =>
|
|
137
|
+
src.includes(pattern)
|
|
138
|
+
)
|
|
139
|
+
if (isWhitelisted) {
|
|
140
|
+
this._debug('Whitelisted, skipping:', src)
|
|
141
|
+
return false
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Check blacklist (always rewrite)
|
|
146
|
+
if (this.config.blacklist.length > 0) {
|
|
147
|
+
const isBlacklisted = this.config.blacklist.some(pattern =>
|
|
148
|
+
src.includes(pattern)
|
|
149
|
+
)
|
|
150
|
+
if (isBlacklisted) {
|
|
151
|
+
return true
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Check if tracker is detected
|
|
156
|
+
const category = this._detectCategory(src)
|
|
157
|
+
return category !== null
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Detect category from script src
|
|
162
|
+
* @param {string} src - Script source URL
|
|
163
|
+
* @returns {string|null} Category name or null
|
|
164
|
+
*/
|
|
165
|
+
_detectCategory(src) {
|
|
166
|
+
const patterns = getAllTrackerPatterns()
|
|
167
|
+
|
|
168
|
+
for (const pattern of patterns) {
|
|
169
|
+
for (const scriptPattern of pattern.scripts) {
|
|
170
|
+
if (src.includes(scriptPattern)) {
|
|
171
|
+
return pattern.category
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return null
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Start MutationObserver to watch for new scripts
|
|
181
|
+
* @private
|
|
182
|
+
*/
|
|
183
|
+
_startObserver() {
|
|
184
|
+
if (typeof MutationObserver === 'undefined') {
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
this.observer = new MutationObserver((mutations) => {
|
|
189
|
+
mutations.forEach((mutation) => {
|
|
190
|
+
mutation.addedNodes.forEach((node) => {
|
|
191
|
+
if (node.nodeType === 1) {
|
|
192
|
+
// Check if it's a script
|
|
193
|
+
if (node.tagName === 'SCRIPT' && node.getAttribute('src')) {
|
|
194
|
+
if (this._shouldRewrite(node)) {
|
|
195
|
+
this._debug('New script detected, rewriting...')
|
|
196
|
+
this._rewriteScript(node)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Check for scripts inside added node
|
|
201
|
+
const scripts = node.querySelectorAll?.('script[src]')
|
|
202
|
+
scripts?.forEach(script => {
|
|
203
|
+
if (this._shouldRewrite(script)) {
|
|
204
|
+
this._rewriteScript(script)
|
|
205
|
+
}
|
|
206
|
+
})
|
|
207
|
+
}
|
|
208
|
+
})
|
|
209
|
+
})
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
this.observer.observe(document.documentElement, {
|
|
213
|
+
childList: true,
|
|
214
|
+
subtree: true
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
this._debug('Script rewriter observer started')
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Get rewritten scripts
|
|
222
|
+
* @returns {Array}
|
|
223
|
+
*/
|
|
224
|
+
getRewrittenScripts() {
|
|
225
|
+
return Array.from(this.rewrittenScripts.entries()).map(([original, rewritten]) => ({
|
|
226
|
+
original: original.src,
|
|
227
|
+
category: rewritten.dataset.cookieCategory,
|
|
228
|
+
rewritten: rewritten.dataset.cookieSrc
|
|
229
|
+
}))
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Get stats
|
|
234
|
+
* @returns {Object}
|
|
235
|
+
*/
|
|
236
|
+
getStats() {
|
|
237
|
+
const scripts = this.getRewrittenScripts()
|
|
238
|
+
const byCategory = {}
|
|
239
|
+
|
|
240
|
+
scripts.forEach(script => {
|
|
241
|
+
if (!byCategory[script.category]) {
|
|
242
|
+
byCategory[script.category] = []
|
|
243
|
+
}
|
|
244
|
+
byCategory[script.category].push(script.original)
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
total: scripts.length,
|
|
249
|
+
byCategory
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Destroy rewriter
|
|
255
|
+
*/
|
|
256
|
+
destroy() {
|
|
257
|
+
if (this.observer) {
|
|
258
|
+
this.observer.disconnect()
|
|
259
|
+
this.observer = null
|
|
260
|
+
}
|
|
261
|
+
this.rewrittenScripts.clear()
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
_debug(...args) {
|
|
265
|
+
// Debug disabled
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Helper function to rewrite scripts automatically
|
|
271
|
+
* @param {Object} config - Configuration
|
|
272
|
+
* @returns {ScriptRewriter}
|
|
273
|
+
*/
|
|
274
|
+
export function initScriptRewriter(config = {}) {
|
|
275
|
+
const rewriter = new ScriptRewriter(config)
|
|
276
|
+
rewriter.initialize()
|
|
277
|
+
return rewriter
|
|
278
|
+
}
|