52 |
Data Collection
53 |
56 |
57 |
58 |
59 | {value}
60 |
61 |
62 | )
63 | }
64 |
--------------------------------------------------------------------------------
/rl.js/data/index.js:
--------------------------------------------------------------------------------
1 | const ADS = {
2 | 0: 'https://m.media-amazon.com/images/I/51jBeCDwMQL.jpg',
3 | 1: 'https://images-na.ssl-images-amazon.com/images/I/81Kr+YIWjCL.jpg',
4 | 2: 'https://d3nuqriibqh3vw.cloudfront.net/styles/aotw_detail_ir/s3/images/northernPhysics.jpg',
5 | }
6 | const ALL_UIOPTIONS = ['books', 'news', 'travel']
7 |
8 | const META_DATA = {
9 | 1: {
10 | base_url: 'book-client',
11 | no_of_clients: 3,
12 | dim: 3,
13 | model_name: 'example_1',
14 | has_nested_route: 'false',
15 | change_policy: false,
16 | change_prob_idxs: { 0: [0], 1: [1] }, // indexes for probability value change
17 | change_probs: { 0: [0.8], 1: [0.9] }, // probability value for different indexes given by change_prob_idxs
18 | },
19 | 2: {
20 | base_url: 'ui-client',
21 | no_of_clients: 1,
22 | dim: 24,
23 | model_name: 'example_2',
24 | description: 'Single client; No preference change',
25 | has_nested_route: 'true',
26 | change_policy: false,
27 | change_prob_idxs: { 0: [0] }, // indexes for probability value change
28 | change_probs: { 0: [0.7] }, // probability value for different indexes given by change_prob_idxs
29 | },
30 | 3: {
31 | base_url: 'ui-client',
32 | no_of_clients: 1,
33 | dim: 24,
34 | model_name: 'example_3',
35 | description: '(Drift)Single client; Change preference during training',
36 | has_nested_route: 'true',
37 | change_policy: true,
38 | change_prob_idxs: { 0: [0, 2] }, // indexes for probability value change
39 | change_probs: { 0: [0.7, 0.9] }, // probability value for different indexes given by change_prob_idxs
40 | time_interval_for_policy_change: 200,
41 | },
42 | 4: {
43 | base_url: 'ui-client',
44 | no_of_clients: 2,
45 | dim: 24,
46 | model_name: 'example_4',
47 | description:
48 | '(Diff)Multiple clients with different preferences; No preference change',
49 | has_nested_route: 'true',
50 | change_policy: false,
51 | change_prob_idxs: { 0: [0], 1: [1] }, // indexes for probability value change
52 | change_probs: { 0: [0.8], 1: [0.8] }, // probability value for different indexes given by change_prob_idxs
53 | },
54 | 5: {
55 | base_url: 'ui-client',
56 | no_of_clients: 2,
57 | dim: 24,
58 | model_name: 'example_5',
59 | description:
60 | '(Drift and Diff)Multiple clients with different preferences; Change preference of first client while training',
61 | has_nested_route: 'true',
62 | change_policy: true,
63 | change_prob_idxs: { 0: [0, 4], 1: [1, 1] }, // indexes for probability value change
64 | change_probs: { 0: [0.7, 0.8], 1: [0.7, 0.7] }, // probability value for different indexes given by change_prob_idxs
65 | time_interval_for_policy_change: 200,
66 | },
67 | 6: {
68 | base_url: 'web-client',
69 | no_of_clients: 1,
70 | dim: 24,
71 | description: 'Web Client; No preference change and one client only',
72 | has_nested_route: 'false',
73 | model_name: 'example_6',
74 | change_policy: false,
75 | change_prob_idxs: { 0: [0] }, // indexes for probability value change
76 | change_probs: { 0: [0.8] }, // probability value for different indexes given by change_prob_idxs
77 | },
78 | 7: {
79 | base_url: 'web-client',
80 | no_of_clients: 2,
81 | dim: 24,
82 | description:
83 | 'Web Client; No preference change and two clients with different probabilities',
84 | has_nested_route: 'false',
85 | model_name: 'example_7',
86 | change_policy: false,
87 | change_prob_idxs: { 0: [0, 4], 1: [1, 1] }, // indexes for probability value change
88 | change_probs: { 0: [0.7, 0.8], 1: [0.7, 0.7] }, // probability value for different indexes given by change_prob_idxs
89 | },
90 | }
91 |
92 | export { ADS, META_DATA, ALL_UIOPTIONS }
93 |
--------------------------------------------------------------------------------
/rl.js/common.js:
--------------------------------------------------------------------------------
1 | import * as tf from '@tensorflow/tfjs'
2 |
3 | const binomial_sample = (accept_rate) => (Math.random() < accept_rate ? 1 : 0)
4 | class Simulator {
5 | constructor(rates) {
6 | this.rates = rates
7 | this.action_space = Array(rates.length)
8 | }
9 | simulate(idx) {
10 | console.log('[Content Script ML - Socket]Rates', this.rates)
11 | console.log('[Content Script ML - Socket]Rate', this.rates[idx])
12 | let choice = binomial_sample(this.rates[idx])
13 | return choice
14 | }
15 | }
16 |
17 | /**
18 | * Get maximum value
19 | */
20 | const argMax = (d) =>
21 | Object.entries(d).filter((el) => el[1] === Math.max(...Object.values(d)))[0][0]
22 |
23 | /**
24 | * Update alphas and betas - Thompson Sampling
25 | * @param {Tensor1D} rewards
26 | * @param {Tensor1D} samples
27 | * @param {Tensor1D} alphas
28 | * @param {Tensor1D} betas
29 | * @returns {Array}
30 | */
31 | const banditThompson = (rewards, samples, alphas, betas) => {
32 | console.log('[Content Script ML - Socket]banditThompson')
33 | const prev_alpha = alphas
34 | const prev_beta = betas
35 |
36 | alphas = prev_alpha.add(rewards)
37 | betas = prev_beta.add(samples.sub(rewards))
38 | return [alphas, betas]
39 | }
40 |
41 | /**
42 | * Calculate gradients
43 | * @param {Tensor1D} alphas
44 | * @param {Tensor1D} betas
45 | * @param {Tensor1D} n_alphas
46 | * @param {Tensor1D} n_betas
47 | * @returns {Array}
48 | */
49 | const calcGradient = (alphas, betas, n_alphas, n_betas) => {
50 | let d_alphas, d_betas
51 | d_alphas = n_alphas.sub(alphas)
52 | d_betas = n_betas.sub(betas)
53 | return [d_alphas, d_betas]
54 | }
55 |
56 | /**
57 | * Simulate user action
58 | * @param {Array} preferences
59 | * @param {number} option_id
60 | * @returns {number}
61 | */
62 | const simulate = (simulated_rates, selectedOption) => {
63 | const env = new Simulator(simulated_rates)
64 | return env.simulate(selectedOption)
65 | }
66 |
67 | const actionAndUpdate = (alphasArray, betasArray, selectedOption, reward) => {
68 | let alphas_betas
69 | let rewardVector = Array(alphasArray.length).fill(0)
70 | let sampledVector = Array(alphasArray.length).fill(0)
71 | console.log('[Content Script ML - Socket]Update Selected Option', selectedOption)
72 | console.log('[Content Script ML - Socket]Reward', reward)
73 | console.log('[Content Script ML - Socket]Alphas, Betas', alphasArray, betasArray)
74 |
75 | console.log(
76 | '[Content Script ML - Socket]alphasArray.length == 0 || betasArray.length == 0 || betasArray.length != alphasArray.length',
77 | alphasArray.length === 0 ||
78 | betasArray.length === 0 ||
79 | betasArray.length !== alphasArray.length
80 | )
81 | if (
82 | alphasArray.length === 0 ||
83 | betasArray.length === 0 ||
84 | betasArray.length !== alphasArray.length
85 | )
86 | return false
87 |
88 | rewardVector[selectedOption] = reward // reward
89 | sampledVector[selectedOption] = 1
90 | console.log(
91 | '[Content Script ML - Socket]Reward Vector, Sampled Vector',
92 | rewardVector,
93 | sampledVector
94 | )
95 | console.log(
96 | '[Content Script ML - Socket]tf.tensor(rewardVector)',
97 | tf.tensor(rewardVector)
98 | )
99 |
100 | alphas_betas = banditThompson(
101 | tf.tensor(rewardVector),
102 | tf.tensor(sampledVector),
103 | tf.tensor(alphasArray),
104 | tf.tensor(betasArray)
105 | )
106 | console.log('[Content Script ML - Socket]alphas_betas', alphas_betas)
107 | let gradWeights = calcGradient(
108 | tf.tensor(alphasArray),
109 | tf.tensor(betasArray),
110 | alphas_betas[0],
111 | alphas_betas[1]
112 | )
113 |
114 | console.log('[Content Script ML - Socket]gradWeights', gradWeights)
115 | return [gradWeights, alphas_betas]
116 | }
117 |
118 | /**
119 | * Generate probabilities of size given by dimension. The probabilities sums up to 1
120 | * @param {number} dim
121 | * @returns {Array}
122 | */
123 | const generateProbabilities = (dim, preference) => {
124 | let probabilities = []
125 | console.log('[Content Script ML - Socket]Preference', preference)
126 |
127 | let prob_for_remaining = (1 - preference[0]) / dim
128 | for (let i = 0; i < dim; i++) {
129 | if (i === preference[1]) {
130 | probabilities.push(preference[0])
131 | } else {
132 | probabilities.push(prob_for_remaining)
133 | }
134 | }
135 | return probabilities
136 | }
137 |
138 | /**
139 | * Generate policies dynamically for given number of clients.
140 | * @param {number} no_of_clients
141 | * @returns {Array}
142 | */
143 | const generatePolicies = (no_of_clients, dim = 24, client_preferences) => {
144 | let policies = []
145 | for (let i = 0; i < no_of_clients; i++) {
146 | let policy = generateProbabilities(dim, client_preferences[i])
147 | policies.push(policy)
148 | }
149 | return policies
150 | }
151 |
152 | /**
153 | * Generate client preferences
154 | * @param {number} no_of_clients
155 | * @returns
156 | */
157 | let clientPreferences = (no_of_clients, index, change_prob_idxs, change_probs) => {
158 | return Array(no_of_clients)
159 | .fill()
160 | .map(
161 | function (x, i) {
162 | return [this.probs[i][this.idx], this.probIdxs[i][this.idx]]
163 | },
164 | {
165 | probIdxs: change_prob_idxs,
166 | probs: change_probs,
167 | idx: index,
168 | }
169 | )
170 | }
171 |
172 | export {
173 | clientPreferences,
174 | generatePolicies,
175 | generateProbabilities,
176 | argMax,
177 | banditThompson,
178 | calcGradient,
179 | simulate,
180 | actionAndUpdate,
181 | }
182 |
--------------------------------------------------------------------------------
/rl.js/index.js:
--------------------------------------------------------------------------------
1 | import { actionAndUpdate, generatePolicies, clientPreferences } from './common'
2 | import { META_DATA } from './data/'
3 | import { track } from './useractivity'
4 | const id = 6 // Example Id
5 | const API_ENDPOINT = '127.0.0.1:8082'
6 | const url = 'ws://' + API_ENDPOINT + '/fl-server/' + META_DATA[id].model_name
7 |
8 | // Features/parameters that determine the users action
9 | let alphasArray = [],
10 | betasArray = [],
11 | policy = [],
12 | clientId = null,
13 | selectedOption = 0,
14 | cycle = 0,
15 | socket = null,
16 | simulation = false,
17 | user_privacy_preference_level = 0,
18 | random = true,
19 | noOfClients = META_DATA[id].no_of_clients,
20 | probIdx = 0, // It increases by 1 unit if the policy change is set to true
21 | gradWeights,
22 | new_reward,
23 | interval,
24 | endOfCycle = false
25 |
26 | let client_preferences = clientPreferences(
27 | noOfClients,
28 | probIdx,
29 | META_DATA[id].change_prob_idxs,
30 | META_DATA[id].change_probs
31 | )
32 | let userActionPromiseResolve = undefined,
33 | userActionPromise = null
34 | // User options : 24 types of options
35 | const dim = META_DATA[id].dim,
36 | stopAfter = 100,
37 | policies = generatePolicies(noOfClients, dim, client_preferences)
38 |
39 | function trackingCallback(collection, properties, callback) {
40 | if (collection === 'clicks') {
41 | if (endOfCycle) return
42 |
43 | let elem = document.getElementById('root')
44 | new_reward = parseInt(elem.getAttribute('data-reward'))
45 | endOfCycle = true
46 | handleUserAction()
47 | }
48 | }
49 |
50 | // Resolve after user action
51 | const handleUserAction = () => {
52 | console.log('[Content Script ML]Action Resolved')
53 | userActionPromiseResolve(true)
54 | }
55 | // Inject Noise
56 | const injectNoise = (user_privacy_preference_level) => {
57 | let random_number = Math.random()
58 | console.log(
59 | '[Content Script ML - Socket]Math.random:user_privacy_preference_level',
60 | random_number,
61 | ':',
62 | user_privacy_preference_level / 100
63 | )
64 | return random_number < user_privacy_preference_level / 100 ? 1 : 0
65 | }
66 |
67 | // Get reward and update weights
68 | const setRewardAndUpdateWeights = async () => {
69 | let alphas,
70 | betas,
71 | clicked = false
72 | userActionPromise = new Promise((resolve) => {
73 | userActionPromiseResolve = resolve
74 | })
75 | if (cycle >= stopAfter) {
76 | clearInterval(interval)
77 | return
78 | }
79 |
80 | // Raise the useraction event to trigger user action in website
81 | let webelem = document.getElementById('root')
82 | let isWaiting = parseInt(webelem.getAttribute('data-waiting')) // Signals website is waiting for 'useraction' event.
83 | console.log(
84 | '[Content Script ML - Socket]Website is waiting for event',
85 | isWaiting,
86 | isWaiting === 1
87 | )
88 |
89 | if (!endOfCycle && isWaiting === 0) return
90 |
91 | // Read selected option from the website
92 | selectedOption = webelem.getAttribute('data-option')
93 | console.log('[Content Script ML - Socket]Selected Option in Website', selectedOption)
94 | webelem.dispatchEvent(new CustomEvent('useraction'))
95 | console.log("[Content Script ML - Socket]Trigger 'useraction' Event", cycle)
96 |
97 | console.log(
98 | '[Content Script ML - Socket]Waiting for user action..................................'
99 | )
100 | clicked = await userActionPromise
101 |
102 | if (clicked === true) {
103 | if (new_reward === 1) {
104 | console.log('[Content Script ML - Socket]Clicked', clicked)
105 | } else {
106 | console.log('[Content Script ML - Socket]Rejected')
107 | }
108 | console.log('[Content Script ML - Socket]New Reward selected', new_reward)
109 |
110 | // Calculate new gradients
111 | let params = actionAndUpdate(alphasArray, betasArray, selectedOption, new_reward)
112 | console.log('[Content Script ML - Socket]Grad Weights', params)
113 | gradWeights = params[0]
114 | alphas = gradWeights[0].dataSync()
115 | betas = gradWeights[1].dataSync()
116 |
117 | // Check if random is true and read the user preference level value to add noise
118 | if (random === true) {
119 | let addNoise = injectNoise(user_privacy_preference_level)
120 | console.log(
121 | '[Content Script ML - Socket]Bernoulli Sampling Result(Toss Result -> Add Noise):',
122 | addNoise
123 | )
124 | if (addNoise) {
125 | alphas = Object.assign({}, Array(alphasArray.length).fill(0))
126 | betas = Object.assign({}, Array(alphasArray.length).fill(0))
127 | console.log('[Content Script ML - Socket]alphas;betas:', alphas, betas)
128 | }
129 | }
130 | console.log('[Content Script ML - Socket]Diff: alphas and betas', alphas, betas)
131 | // Send data to the server
132 | console.log('[Content Script ML - Socket]Sending New Gradients to the Server')
133 | endOfCycle = false
134 | socket.send(
135 | JSON.stringify({
136 | event: 'update', // 0 -> event
137 | alphas: alphas, // 1 -> alphas
138 | betas: betas, // 2 -> betas
139 | client_id: clientId,
140 | model_name: 'example_' + id,
141 | })
142 | )
143 | }
144 | }
145 |
146 | // Initializes socket events
147 | const initialize = (clientId) => {
148 | console.log('[Content Script ML - Socket]Onopen event registered')
149 | // Connect to the server & Get params from server
150 | socket.onopen = (message) => {
151 | console.log('[Content Script ML - Socket]Connecton established')
152 | console.log('[Content Script ML - Socket]Message Received', message)
153 | socket.send(
154 | JSON.stringify({
155 | event: 'connected',
156 | client_id: clientId,
157 | model_name: 'example_' + id,
158 | })
159 | )
160 | }
161 |
162 | console.log('[Content Script ML - Socket]Onmessage event registered')
163 | // Events to receive message from server
164 | socket.onmessage = (event) => {
165 | const message_from_server = JSON.parse(event.data)
166 | // console.log("[Content Script ML - Socket]Message Received", message_from_server);
167 |
168 | if (cycle > stopAfter) return
169 |
170 | let dim_from_server = null
171 | console.log('[Content Script ML - Socket]Message Received', message_from_server)
172 | console.log('[Content Script ML - Socket]Type', message_from_server['type'])
173 | // Sets params with the value received from the server
174 | if (message_from_server['type'] === 'init-params') {
175 | dim_from_server = message_from_server.params['dim']
176 | // Set the values
177 | if (dim_from_server !== dim) {
178 | console.log('[Content Script ML - Socket]Dimension Doesnot Match. ')
179 | return
180 | }
181 | alphasArray = message_from_server.params['al']
182 | betasArray = message_from_server.params['bt']
183 | console.log(
184 | '[Content Script ML - Socket]Params Received Alphas and Betas',
185 | alphasArray,
186 | betasArray
187 | )
188 | // setRewardAndUpdateWeights();
189 | } else if (message_from_server['type'] === 'new_weights') {
190 | alphasArray = message_from_server.params['al']
191 | betasArray = message_from_server.params['bt']
192 | userActionPromiseResolve = undefined
193 | // Sync with website: Update the id of the root element of the website to
194 | // trigger start of new cycle.
195 | cycle = cycle + 1
196 | let webelem = document.getElementById('root')
197 | webelem.setAttribute('data-cycle', cycle) // Update cycle to website
198 | webelem.dispatchEvent(new CustomEvent('newcycle', { detail: { cycle: cycle } }))
199 | console.log('[Content Script ML - Socket]Trigger Newcycle Event, Cycle =>', cycle)
200 | // setRewardAndUpdateWeights();
201 | }
202 | }
203 | // Set timer to run reward and update
204 | interval = setInterval(setRewardAndUpdateWeights, 1000)
205 | }
206 |
207 | // Initialization
208 | socket = new WebSocket(url)
209 | clientId = 0 //Math.floor(Math.random() * noOfClients);
210 | policy = policies[clientId]
211 |
212 | // Display Params
213 | console.log('[Content Script ML]Example ID', id)
214 | console.log('[Content Script ML]Client ID:', clientId)
215 | console.log('[Content Script ML]noOfClients', noOfClients)
216 | console.log('[Content Script ML]API_ENDPOINT', API_ENDPOINT)
217 | console.log('[Content Script ML]URL', url)
218 | console.log('[Content Script ML]Simulate', simulation)
219 | console.log('[Content Script ML]Cycle', cycle)
220 | console.log('[Content Script ML]stopAfter', stopAfter)
221 | console.log('[Content Script ML - Socket]Selected Policy:', policy)
222 |
223 | // Initialize the listeners for socket events
224 | initialize(clientId)
225 | track(trackingCallback)
226 |
227 | // Listener for the messages sent from background script.
228 | browser.runtime.onMessage.addListener((data, sender) => {
229 | console.log(
230 | '[Content Script ML - Socket]Message from the background script, sender',
231 | data,
232 | sender
233 | )
234 | console.log(
235 | '[Content Script ML - Socket]data - mtype:value',
236 | data['_mtype'],
237 | ':',
238 | data['_message']
239 | )
240 | user_privacy_preference_level = data['_message']
241 |
242 | return Promise.resolve({
243 | status: 'success',
244 | })
245 | })
246 |
--------------------------------------------------------------------------------
/rl.js/useractivity.js:
--------------------------------------------------------------------------------
1 | export function track(callback) {
2 | var options = {
3 | pageviewsEventName: 'pageviews',
4 | inputChangeEventName: 'input-changes',
5 | clicksEventName: 'clicks',
6 | formSubmissionsEventName: 'form-submissions',
7 | callbackTimeout: 1000,
8 | globalProperties: {
9 | page_url: window.location.href,
10 | referrer_url: document.referrer,
11 | },
12 | }
13 | // create a common namespace with options
14 | var CommonWeb = {
15 | options: options,
16 | }
17 |
18 | CommonWeb.addGlobalProperties = function (properties) {
19 | $.extend(CommonWeb.options.globalProperties, properties)
20 | }
21 |
22 | // initiate user tracking, using a GUID stored in a cookie
23 | // The user can pass in a custom cookie name and custom GUID, if they would like
24 | CommonWeb.trackSession = function (cookieName, defaultGuid) {
25 | if (typeof cookieName !== 'string') {
26 | cookieName = 'common_web_guid'
27 | }
28 |
29 | // Look for the GUID in the currently set cookies
30 | var cookies = document.cookie.split('; ')
31 | var guid = null
32 |
33 | for (var i = 0; i < cookies.length; i++) {
34 | cookieParts = cookies[i].split('=')
35 | if (cookieParts[0] === cookieName) {
36 | // Got it!
37 | guid = cookieParts[1]
38 | break
39 | }
40 | }
41 |
42 | // We didn't find our guid in the cookies, so we need to generate our own
43 | if (guid === null) {
44 | if (typeof defaultGuid === 'string') {
45 | guid = defaultGuid
46 | } else {
47 | genSub = function () {
48 | return Math.floor((1 + Math.random()) * 0x10000)
49 | .toString(16)
50 | .substring(1)
51 | }
52 |
53 | guid =
54 | genSub() +
55 | genSub() +
56 | '-' +
57 | genSub() +
58 | '-' +
59 | genSub() +
60 | '-' +
61 | genSub() +
62 | '-' +
63 | genSub() +
64 | genSub() +
65 | genSub()
66 | }
67 |
68 | expiration_date = new Date()
69 | expiration_date.setFullYear(expiration_date.getFullYear() + 1)
70 |
71 | cookie_string =
72 | cookieName + '=' + guid + '; path/; expires=' + expiration_date.toGMTString()
73 | document.cookie = cookie_string
74 | }
75 |
76 | CommonWeb.addGlobalProperties({
77 | guid: guid,
78 | })
79 |
80 | return guid
81 | }
82 |
83 | // setup pageview tracking hooks, optionally including more properties
84 | // more properties can also be a function
85 | // do not double set along with track!
86 | CommonWeb.trackPageview = function (moreProperties) {
87 | var defaultProperties = CommonWeb.options.globalProperties
88 | var properties = $.extend(true, {}, defaultProperties, toProperties(moreProperties))
89 |
90 | CommonWeb.Callback(CommonWeb.options.pageviewsEventName, properties)
91 | }
92 |
93 | CommonWeb.trackClicks = function (elements, moreProperties) {
94 | if (typeof elements === 'undefined') {
95 | elements = $('a')
96 | }
97 | $.each(elements, function (index, element) {
98 | $(element).on('click', function (event) {
99 | var timer = CommonWeb.options.callbackTimeout
100 |
101 | // combine local and global moreProperties
102 | var properties = toClickProperties(event, element, moreProperties)
103 |
104 | // check if the page is probably going to unload
105 | var pageWillUnload =
106 | element.href && element.target !== '_blank' && !isMetaKey(event)
107 | var unloadCallback = function () {}
108 |
109 | // if the page will unload, don't let the JS event bubble
110 | // but navigate to the href after the click
111 | if (pageWillUnload) {
112 | unloadCallback = function () {
113 | window.location.href = element.href
114 | }
115 |
116 | setTimeout(function () {
117 | window.location.href = element.href
118 | }, timer)
119 | }
120 | CommonWeb.Callback(options.clicksEventName, properties, unloadCallback)
121 | })
122 | })
123 | $.each($('button'), function (index, element) {
124 | $(element).on('click', function (event) {
125 | var properties = toClickProperties(event, element, moreProperties)
126 | CommonWeb.Callback(options.clicksEventName, properties)
127 | })
128 | })
129 | }
130 |
131 | // track things that are not links; i.e. don't need any special tricks to
132 | // prevent page unloads
133 | CommonWeb.trackClicksPassive = function (elements, moreProperties) {
134 | $.each(elements, function (index, element) {
135 | $(element).on('click', function (event) {
136 | var properties = toClickProperties(event, element, moreProperties)
137 | CommonWeb.Callback(options.clicksEventName, properties)
138 | })
139 | })
140 | }
141 |
142 | // track form submissions
143 | CommonWeb.trackFormSubmissions = function (elements, moreProperties) {
144 | if (typeof elements === 'undefined') {
145 | elements = $('form')
146 | }
147 |
148 | $.each(elements, function (index, element) {
149 | var timer = CommonWeb.options.callbackTimeout
150 |
151 | // use to avoid duplicate submits
152 | var callbackCalled = false
153 |
154 | $(element).on('submit', function (event) {
155 | var properties = toSubmitProperties(event, element, moreProperties)
156 |
157 | // assume true for now in this method
158 | var pageWillUnload = true
159 | var unloadCallback = function () {}
160 |
161 | if (pageWillUnload) {
162 | unloadCallback = function () {
163 | // not the best approach here.
164 | // the form can only be submitted
165 | // once, etc.
166 | if (!callbackCalled) {
167 | callbackCalled = true
168 | element.submit()
169 | }
170 | }
171 |
172 | event.preventDefault()
173 |
174 | // We only want to fire the timeout if
175 | // we know the page will unload. Ajax
176 | // form submissions shouldn't submit.
177 | setTimeout(function () {
178 | callbackCalled = true
179 | element.submit()
180 | }, timer)
181 | }
182 |
183 | CommonWeb.Callback(options.formSubmissionsEventName, properties, unloadCallback)
184 | })
185 | })
186 | }
187 |
188 | CommonWeb.trackInputChanges = function (elements, moreProperties) {
189 | if (typeof elements === 'undefined') {
190 | elements = $('input, textarea, select')
191 | }
192 |
193 | $.each(elements, function (index, element) {
194 | var currentValue = $(element).val()
195 |
196 | $(element).on('change', function (event) {
197 | var properties = toChangeProperties(event, element, currentValue, moreProperties)
198 | CommonWeb.Callback(options.inputChangeEventName, properties)
199 |
200 | currentValue = $(element).val()
201 | })
202 | })
203 | }
204 |
205 | // define a namespace just for transformations of events and elements to properties
206 | // override as a workaround to add / remove properties
207 | CommonWeb.Transformations = {
208 | eventToProperties: function (event) {
209 | var properties = {}
210 |
211 | properties['timestamp'] = event.timestamp
212 | properties['type'] = event.type
213 | properties['metaKey'] = event.metaKey
214 |
215 | return properties
216 | },
217 |
218 | elementToProperties: function (element, extraProperties) {
219 | var properties = extraProperties || {}
220 |
221 | // add the tag name
222 | properties.tagName = element.tagName
223 |
224 | // add the inner text for some tag types
225 | if (element.tagName === 'A') {
226 | properties.text = element.innerText
227 | }
228 |
229 | // add each attribute
230 | $(element.attributes).each(function (index, attr) {
231 | properties[attr.nodeName] = attr.value
232 | })
233 |
234 | // break classes out into an array if any exist
235 | var classes = $(element).attr('class')
236 | if (classes) {
237 | properties['classes'] = classes.split(/\s+/)
238 | }
239 |
240 | properties['path'] = $(element).getPath()
241 | return properties
242 | },
243 |
244 | formElementToProperties: function (formElement) {
245 | var formValues = {}
246 |
247 | // TODO: remove dependency on jQuery
248 | formValues.form_values = $(formElement).serializeArray()
249 | // simple alias for now, but could do more as
250 | // far as the form values are concerned
251 | return this.elementToProperties(formElement, formValues)
252 | },
253 |
254 | inputElementToProperties: function (inputElement) {
255 | var inputValues = {
256 | value: $(inputElement).val(),
257 | }
258 |
259 | var parentForm = $(inputElement).closest('form')
260 | if (parentForm.size() > 0) {
261 | inputValues.form = this.elementToProperties(parentForm[0])
262 | }
263 |
264 | return this.elementToProperties(inputElement, inputValues)
265 | },
266 | }
267 |
268 | function toClickProperties(event, element, moreProperties) {
269 | var defaultProperties = CommonWeb.options.globalProperties
270 | var properties = $.extend(
271 | true,
272 | {},
273 | defaultProperties,
274 | toProperties(moreProperties, [event, element])
275 | )
276 |
277 | var elementProperties = {
278 | element: CommonWeb.Transformations.elementToProperties(element, null),
279 | }
280 | var eventProperties = {
281 | event: CommonWeb.Transformations.eventToProperties(event),
282 | }
283 |
284 | return $.extend(true, {}, properties, elementProperties, eventProperties)
285 | }
286 |
287 | function toChangeProperties(event, element, previousValue, moreProperties) {
288 | var defaultProperties = CommonWeb.options.globalProperties
289 | var properties = $.extend(
290 | true,
291 | {},
292 | defaultProperties,
293 | toProperties(moreProperties, [event, element])
294 | )
295 |
296 | var elementProperties = {
297 | element: CommonWeb.Transformations.inputElementToProperties(element),
298 | }
299 | if (previousValue && previousValue !== '') {
300 | elementProperties.element.previousValue = previousValue
301 | }
302 |
303 | var eventProperties = {
304 | event: CommonWeb.Transformations.eventToProperties(event),
305 | }
306 |
307 | return $.extend(true, {}, properties, elementProperties, eventProperties)
308 | }
309 |
310 | function toSubmitProperties(event, element, moreProperties) {
311 | var defaultProperties = CommonWeb.options.globalProperties
312 | var properties = $.extend(
313 | true,
314 | {},
315 | defaultProperties,
316 | toProperties(moreProperties, [event, element])
317 | )
318 |
319 | var elementProperties = {
320 | element: CommonWeb.Transformations.formElementToProperties(element),
321 | }
322 | var eventProperties = {
323 | event: CommonWeb.Transformations.eventToProperties(event),
324 | }
325 |
326 | return $.extend(true, {}, properties, elementProperties, eventProperties)
327 | }
328 |
329 | function toProperties(propertiesOrFunction, args) {
330 | if (typeof propertiesOrFunction === 'function') {
331 | return propertiesOrFunction.apply(window, args)
332 | } else {
333 | return propertiesOrFunction
334 | }
335 | }
336 |
337 | function isMetaKey(event) {
338 | return event.metaKey || event.altKey || event.ctrlKey || event.shiftKey
339 | }
340 |
341 | /*
342 | jQuery-GetPath v0.01, by Dave Cardwell. (2007-04-27)
343 | http://davecardwell.co.uk/javascript/jquery/plugins/jquery-getpath/
344 | Copyright (c)2007 Dave Cardwell. All rights reserved.
345 | Released under the MIT License.
346 | Usage:
347 | var path = $('#foo').getPath();
348 | */
349 | jQuery.fn.extend({
350 | getPath: function (path) {
351 | // The first time this function is called, path won't be defined.
352 | if (typeof path == 'undefined') path = ''
353 |
354 | // If this element is we've reached the end of the path.
355 | if (this.is('html')) return 'html' + path
356 |
357 | // Add the element name.
358 | var cur = this.get(0).nodeName.toLowerCase()
359 |
360 | // Determine the IDs and path.
361 | var id = this.attr('id'),
362 | klass = this.attr('class')
363 |
364 | // Add the #id if there is one.
365 | if (typeof id != 'undefined') cur += '#' + id
366 |
367 | // Add any classes.
368 | if (typeof klass != 'undefined') cur += '.' + klass.split(/[\s\n]+/).join('.')
369 |
370 | // Recurse up the DOM.
371 | return this.parent().getPath(' > ' + cur + path)
372 | },
373 | })
374 |
375 | // // backends
376 | // CommonWeb.Endpoint = {
377 | // Debug: true,
378 | // Callback: function(collection, properties, callback) {
379 | // console.log("[Content Script Tracking - Callback]", collection, properties, callback);
380 | // window.postMessage({
381 | // type: "FROM_PAGE",
382 | // collection: collection
383 | // });
384 | // console.log("After post message")
385 | // // CommonWeb.Keen.Client.addEvent(collection, properties, function() {
386 | // // if (CommonWeb.Keen.Debug) {
387 | // // console.log(collection + ": " + JSON.stringify(properties));
388 | // // }
389 | // // if (callback) {
390 | // // callback();
391 | // // }
392 | // // });
393 | // }
394 | // };
395 |
396 | window.CommonWeb = CommonWeb
397 | // window.CommonWeb.Callback = CommonWeb.Endpoint.Callback;
398 | window.CommonWeb.Callback = callback
399 | window.CommonWeb.trackClicks()
400 | window.CommonWeb.trackClicksPassive($('div'))
401 | window.CommonWeb.trackPageview()
402 | }
403 |
--------------------------------------------------------------------------------
/public/jquery.min.js:
--------------------------------------------------------------------------------
1 | /*! jQuery v3.6.0 | (c) OpenJS Foundation and other contributors | jquery.org/license */
2 | !function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.0",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0