├── .babelrc ├── .gitignore ├── .meta ├── logui.svg └── tudelft.svg ├── CHANGELOG.md ├── PROTOCOL.md ├── README.md ├── build ├── env │ ├── controller.js │ ├── index.html │ ├── normalize.css │ └── styles.css └── reactapp │ ├── .babelrc │ ├── app │ ├── app.js │ └── root.js │ ├── package-lock.json │ └── package.json ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── main.js └── modules │ ├── DOMHandler │ ├── DOMPropertiesObject.js │ ├── binder.js │ ├── browserEventsController.js │ ├── handler.js │ ├── helpers.js │ └── mutationObserverController.js │ ├── browserEvents │ ├── contextMenu.js │ ├── cursorPosition.js │ ├── pageFocus.js │ ├── scroll.js │ ├── urlChange.js │ └── viewportResize.js │ ├── config.js │ ├── defaults.js │ ├── dispatchers │ ├── consoleDispatcher.js │ └── websocketDispatcher.js │ ├── eventCallbackHandler.js │ ├── eventHandlerController.js │ ├── eventHandlers │ ├── formSubmission.js │ ├── mouseClick.js │ ├── mouseHover.js │ ├── sampleEventHandler.js │ └── scrollable.js │ ├── eventPackager.js │ ├── helpers.js │ ├── metadataHandler.js │ ├── metadataSourcers │ ├── elementAttribute.js │ ├── elementProperty.js │ ├── localStorage.js │ ├── reactComponentProp.js │ ├── reactComponentState.js │ └── sessionStorage.js │ ├── required.js │ ├── specificFrameworkEvents.js │ └── validationSchemas.js └── tests ├── env └── landing.html └── modules └── sample.spec.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/env" 4 | ], 5 | "plugins": [ 6 | ["import-directory"], 7 | ["@babel/plugin-transform-runtime", { 8 | "regenerator": true 9 | }] 10 | ] 11 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # System databases 2 | Thumbs.db 3 | .DS_Store 4 | 5 | # Node/npm Specifics 6 | node_modules/ 7 | .npm 8 | .env 9 | 10 | # VSCode 11 | .vscode/* 12 | !.vscode/settings.json 13 | !.vscode/tasks.json 14 | !.vscode/launch.json 15 | !.vscode/extensions.json 16 | 17 | # Repository-specific rules 18 | build/*.bundle.js 19 | build/ 20 | tests/screenshots 21 | tests/logui.test.bundle.js -------------------------------------------------------------------------------- /.meta/logui.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 22 | 23 | 47 | 48 | 72 | 73 | -------------------------------------------------------------------------------- /.meta/tudelft.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 13 | 17 | 19 | 21 | 30 | 34 | 38 | 41 | 43 | 46 | 47 | 49 | 55 | 57 | 59 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # LogUI Changelog 2 | 3 | This Markdown file contains the `CHANGELOG` for the LogUI client library. Changes are made as per the [GNU CHANGELOG Style Guide](https://www.gnu.org/prep/standards/html_node/Style-of-Change-Logs.html). 4 | 5 | ``` 6 | 7 | 2020-09-14 Version 0.4.0 8 | 9 | Implemented basic repository structure, including working build and test environment. Repository ready for the addition of existing library code. 10 | 11 | * NPM is the selected package manager. 12 | * package.json contains the necessary infrastructure to build and test the project. 13 | * Sample test sample.spec.js included. 14 | * .gitignore is included. 15 | 16 | 2021-03-26 Version 0.5.0 17 | 18 | Basic implementation now complete. All basic functionality has been included, along with communicative ability to the LogUI server component. 19 | 20 | 2021-03-26 Version 0.5.1 21 | 22 | Works with LogUI server version 0.5.1 and above. 23 | 24 | Altered the configuration object to include an authorisation token, not an authentication token. Tidying up terminology. 25 | 26 | 2021-03-31 Version 0.5.2a 27 | 28 | Works with LogUI server version 0.5.1 and above. 29 | 30 | Altered the behaviour of the eventCallbackHandler to prevent event bubbling. 31 | Updated the websocketDispatcher to suppress logging when verbose is set to false. 32 | ``` -------------------------------------------------------------------------------- /PROTOCOL.md: -------------------------------------------------------------------------------- 1 | # LogUI Communication Protocol 2 | 3 | This document outlines the protocol that exists between the LogUI Client and LogUI Server. **Consider this page to be the definitive guide for the LogUI communication protocol.** 4 | 5 | Last Updated | Changed By 6 | -------------|-------------- 7 | 2020-09-15 | David Maxwell 8 | 9 | ## TODOs 10 | 11 | - [x] Outline protocol 12 | - [x] Provide error codes 13 | - [ ] Include description of minimum viable event description 14 | - [ ] Diagram of process 15 | - [x] WebSocket Connection 16 | - [x] LogUI Handshake 17 | - [x] Event Listening 18 | - [x] Application-Specific Changing 19 | - [x] WebSocket Disconnection 20 | 21 | ## Protocol Stages 22 | 23 | The protocol establishes a sequence of stages that take place over the WebSocket connection between the client and server. All exchanges take place via standard WebSocket calls after the HTTP layer establishes a connection. 24 | 25 | 1. **WebSocket Connection** 26 | The LogUI client creates a connection to the LogUI logging endpoint. 27 | 28 | 2. **LogUI Handshake** 29 | The handshake that establishes identity. 30 | 31 | 3. **Event Listening** 32 | The server then waits for events and logs them as they are sent by the client. The client may also request to update the [application-specific information](#logui-handshake) stored for each logged event. 33 | 34 | 4. **WebSocket Disconnection** 35 | Disconnect. The disconnect can be triggered by either the client or the server. 36 | 37 | Of course, there are several possible responses to each of the above stages. Things can go wrong! Hopefully, this protocol allows each party to be aware of each other's state, and act accordingly. We now take each stage in turn, providing detailed explanations of what exactly happens during these stages. We also include example messages to demonstrate the functionality of the procotol. 38 | 39 | ### WebSocket Connection 40 | This stage is handled by the [WebSockets Web API](https://developer.mozilla.org/en-US/docs/Web/API/Websockets_API). A connection to the LogUI logging endpoint is established by the aforementioned APIs. Once a connection has been established, we move to the LogUI handshake stage. 41 | 42 | ### LogUI Handshake 43 | The LogUI handshake stage provides the server with basic authentication details that allow the server to determine whether or not it should be listening to the client attempting to establish a connection. Information sent to the server includes the `appIdentifier` string, an encrypted string that when decrypted reveals what application is being logged, and the version of the LogUI client library being used. Information can be checked by the server against a database of known applications. If anything doesn't match, the server will refuse to serve the client and disconnect. 44 | 45 | The LogUI handshake **must be the first message sent by the client.** If anything else is sent down the WebSocket to the server, the server will disconnect immediately. If the server does not receive the handshake within the first three seconds, the server will also disconnect. 46 | 47 | #### Complete LogUI Handshake Example 48 | A complete LogUI handshake request sent by the client, complete with sample data, is shown below. **Note that `appIdentifier` is expanded to its full representation** (it would nominally be represented by a single, encrypted string -- more below). 49 | 50 | ```json 51 | { 52 | "messageType": "logui-handshake-request", 53 | "sessionUUID": "ce2a6120-a78e-45e9-86c7-29df8225494d", 54 | "clientTimestamp": "641143800", 55 | "clientVersion": "0.4.0", 56 | "applicationIdentifier": { // Nominally represented as a single string 57 | "applicationID": "9587489a-2bc3-4f49-a795-b2298408fc49", 58 | "flightID": "fc7af2c8-4d39-4ad0-b287-7a2c1e3a60b1", 59 | "expectedClientVersion": "0.4.0" 60 | }, 61 | "applicationSpecificData": { 62 | "userID": "exp-user-26", 63 | "condition": "c2", 64 | "askedForHelp": true 65 | } 66 | } 67 | ``` 68 | 69 | Let's now walk through each of the individual components of this payload sent to the server. We'll discuss what each key/value pairing represents, and what other values can be provided (for example, if no `sessionUUID` is available). 70 | 71 | ##### `messageType` 72 | This field is a requirement for all messages sent from the LogUI to the server. A `messageType` of `logui-handshake-request` indicates to the server that the remainder of the message will contain information pertaining to a handshake. As such, the server then knows what other fields to expect. 73 | 74 | ##### `sessionUUID` 75 | The `sessionUUID` is a unique session identifier. It identifies the browser tab that has been open, if previous pages with LogUI tracking on the same origin were present. The `sessionUUID` is handled entirely by the client-side library; refer to the documentation of that library for more information. If *no previous page has been loaded* in the same tab or browser session, the value for `sessionUUID` will be set to `null`. 76 | 77 | ##### `clientTimestamp` 78 | The field `clientTimestamp` must provide the UNIX timestamp for the browser at the point the client sends the handshake request. This timestamp is used by the LogUI server to synchronise the client's time against the server's time, and provides a point in time that logged events can be measured from in absolute terms. 79 | 80 | ##### `clientVersion` 81 | `clientVersion` reports the version of the LogUI client library that is being used to issue the handshake request. In the example above, version `0.4.0` is shown. If the version of the LogUI client library is not compatible with the LogUI server being connected to, the handshake is considered invalid. 82 | 83 | ##### `applicationIdentifier` 84 | The `applicationIdentifier` field contains a number of subfields that are used by the LogUI server to verify who is attempting to authenticate. The following fields are what the complete data structure should look like when decrypted by the LogUI server. 85 | 86 | ###### `applicationID` 87 | The `applicationID` is the identifier for the specific application entry in the LogUI server database. If the supplied identifier does not match, the handshake is assumed to be invalid. 88 | 89 | It should be also noted that a given `applicationID` is *tied against a specific domain.* If the domain the server that is hosting the LogUI client library does not match the recorded domain, the handshake is also assumed to be invalid. This is to prevent an `applicationID` code being hijacked and used elsewhere. 90 | 91 | ###### `flightID` 92 | The `flightID` is a sub-identifier for the application, allowing for different configurations of application within the same `applicationID`. Like the `applicationID` above, a comparison is made against the LogUI server's database. If no match is found, the handshake is considered to be invalid. 93 | 94 | ###### `expectedClientVersion` 95 | The final field `expectedClientVersion` dictates what version of the LogUI client has been paired with the `applicationID`. This is to allow for the tying of one specific version of LogUI to an application. If the expected version is not what is reported by the LogUI client itself (in `clientVersion` above), the handshake is rejected. 96 | 97 | ###### (Encrypted) String Representation 98 | Given the sensitive nature of the above three fields, information in `applicationIdentifier` is not sent to the LogUI server in its expanded state. Rather, the `applicationIdentifier` object is converted to a string and encrypted, with this string being sent to the LogUI server on a handshake request instead. This string will be acquired by the developer wishing to integrate LogUI into their web application. 99 | 100 | To give a complete example of `applicationIdentifier`, the above example is encrypted to yield this portion of the handshake request. 101 | 102 | ```json 103 | { 104 | ... 105 | "applicationIdentifier": "ZXlKaGNIQnNhV05oZEdsdmJrbEVJam9pT1RVNE56UTRPV0V0TW1Kak15MDBaalE1TFdFM09UVXRZakl5T1RnME1EaG1ZelE1SWl3aVpteHBaMmgwU1VRaU9pSm1ZemRoWmpKak9DMDBaRE01TFRSaFpEQXRZakk0TnkwM1lUSmpNV1V6WVRZd1lqRWlMQ0psZUhCbFkzUmxaRU5zYVdWdWRGWmxjbk5wYjI0aU9pSXdMalF1TUNKOToxa0lDS206Ym9rWkF3d3lLeU10ZTcwUGZ5N3JkZTBValgwVk9RN2JyTHgwUDY2X1IzNA==", 106 | ... 107 | } 108 | ``` 109 | 110 | ##### `applicationSpecificData` 111 | This final field of the handshake again consists of subfields. Here, the developer who is using LogUI can include additional information to be included as part of log events that are specific to the developer's application. For example, the developer may have an application-specific `userID` that they wish to include, or a `condition` or `askedForHelp` field. These values will be stored with each logged event. Nested fields can also be included if this is desired -- the entire set of properties are transferred to each logged event. 112 | 113 | If no application-specific fields are required, this field **must** be present; simply present an empty pair of JSON curly braces, like so. 114 | 115 | ```json 116 | { 117 | ... 118 | "applicationSpecificData": {} 119 | ... 120 | } 121 | ``` 122 | 123 | #### Valid Handshake Request 124 | If the `logui-handshake-request` sent to the LogUI server is considered to be valid, the server will then send a response back down the connected WebSocket like the one below. 125 | 126 | ```json 127 | { 128 | "messageType": "logui-handshake-success", 129 | "sessionIdentifier": "ce2a6120-a78e-45e9-86c7-29df8225494d" 130 | } 131 | ``` 132 | 133 | This simple response denotes that the handshake request was a success. 134 | 135 | In addition to the successful handshake indication, the response also provides an additional field, `sessionIdentifier`. If this was provided as part of the original handshake request, the same UUID will be provided here. **However, if no UUID was given in the original handshake request, the UUID provided in `sessionIdentifier` denotes a new session identifier.** This UUID should be stored and sent as part of handshake requests for future initialisations of LogUI (of course, only within the same browser session). 136 | 137 | Once this message has been sent from the LogUI server, one can assume that the server then transitions to the next stage, [Event Listening](#event-listening). 138 | 139 | #### Bad Handshake Request 140 | If the handshake request fails for whatever reason, a response is returned that outlines the cause of the failure. 141 | 142 | ```json 143 | { 144 | "messageType": "logui-handshake-failure", 145 | "failureDetails": { 146 | "failureCode": 10, 147 | "terminateConnection": true 148 | } 149 | } 150 | ``` 151 | 152 | This `messageType` indicates that the handshake failed (`logui-handshake-failure`). A more specific `failureDetails` field is provided with two subfields. Refer to [later in this guide](#logui-handshake-failure) for more information on failure responses. 153 | 154 | After this response has been sent to the client, the server will close the WebSocket. 155 | 156 | ### Event Listening 157 | 158 | Event listening is the main stage in LogUI. Interactions that are requested to be logged are gathered by the LogUI client and then sent down the WebSocket connection to the LogUI server in batches. When the LogUI client is ready to send a batch of logged events, it does so with a `logui-event-payload` message. 159 | 160 | An example of this message is shown below. 161 | 162 | ```json 163 | { 164 | "messageType": "logui-event-payload", 165 | "events": [ 166 | { 167 | "timestamp": "123456789", 168 | "eventName": "click" 169 | }, 170 | { 171 | ... 172 | }, 173 | { 174 | ... 175 | }, 176 | ... 177 | ] 178 | } 179 | ``` 180 | 181 | For this message, the type of the aforementioned `logui-event-payload`. The field `events` is an array of events. This array can be of variable length; it can be zero length, which denotes that no events are to be saved from this payload. Events are assumed by the LogUI server to be placed in chronological order (where the earliest event is placed first in the array). 182 | 183 | As each `event` that is logged can vary wildly, the fields that are included in each `event` entry are very much open to whatever is required. However, the LogUI server will expect at the very least the following fields to be present for each event logged. 184 | 185 | * `timestamp`, representing the UNIX timestamp (using the client's time) for when the event in question occurred. 186 | * `eventName`, a string representing the name of the event. 187 | * **TODO** - complete this list as required. 188 | 189 | Application-specific data (as provided by `applicationSpecificData`) is bound to each event on the LogUI server before it is committed to data storage. 190 | 191 | If any of the required fields listed above are missing, or some other formatting issue is present within the request, the request is counted as a [*bad request*. See a later section on this](#dealing-with-bad-requests). 192 | 193 | If the request is successful, the server will respond with a simplistic message, as shown below. 194 | 195 | ```json 196 | { 197 | "messageType": "logui-events-saved" 198 | } 199 | ``` 200 | 201 | This `logui-events-saved` message is an indication that the request has been accepted, and that the events have been successfully stored. As such, the client no longer needs to retain these events in its memory. 202 | 203 | ### Updating Application-Specific Data 204 | 205 | At any stage after a successful handshake, but before the WebSocket connection is closed, the LogUI client may request to change the `applicationSpecificData` that is held for the current session. When a change is requested, all events that are logged will from that point onwards will have the updated set of `applicationSpecificData` applied. 206 | 207 | Why would one want to do this? One possible reason could be to capture a change in state within a particular session. If a user is undertaking an experiment for example, a change in `applicationSpecificData` could reflect the fact that they leave a phase providing instructions to a phase that requires them to perform some kind of activity. Providing a field in `applicationSpecificData` could make this easier to track. 208 | 209 | As the LogUI client works by sending events to the LogUI server in batch, there may be events present that the client wishes to save with the old `applicationSpecificData` scheme, *before* applying an update. To counter this, this request considers two main payloads: the updated `applicationSpecificData` fields, and a `logui-event-payload`. 210 | 211 | An example of this request is shown below. 212 | 213 | ```json 214 | { 215 | "messageType": "logui-application-specific-data-change", 216 | "applicationSpecificDataChanges": { 217 | ..., 218 | "condition": "c3", 219 | "bonus": true, 220 | "askedForHelp": null 221 | ... 222 | }, 223 | "saveEventsBefore": { 224 | "messageType": "logui-event-payload", 225 | "events": [ 226 | { 227 | "timestamp": "123456789", 228 | "eventName": "click" 229 | }, 230 | ... 231 | ] 232 | } 233 | } 234 | ``` 235 | 236 | This request outlines that the application-specific fields `condition` must be set to `c3`, and `bonus` must be set to `true`. Using the `applicationSpecificData` definition [provided earlier in this guide](#complete-logui-handshake-example), we note that `condition` was already provided (with a value of `c2`). The effect of this latter `logui-application-specific-data-change` is that the value of `condition` is **changed** from `c2` to `c3`. As `bonus` did not exist, it is **created**. Where a field existed but should now be **deleted**, the value should be set to `null`, as shown in the example above for `askedForHelp`. 237 | 238 | Setting a field's value to `null` that was not present in the application-specific data beforehand has no effect. If `applicationSpecificDataChanges` is empty, no changes to the application-specific data are made. 239 | 240 | The LogUI client should also provide a `saveEventsBefore` field as part of the `logui-application-specific-data-change` request. The expected value for this field is an encapsulated `logui-event-payload` request, complete with `messageType`. [Refer to the appropriate section for more information on what is expected here](#event-listening). To clarify, the `events` array can be empty (i.e. zero-sized), but *it must always be present in the request.* **All events presented here are saved *before* the `applicationSpecificDataChanges` are applied.** 241 | 242 | If a valid `logui-application-specific-data-change` request is made, the server will respond with a simplistic acknowledgement. This is to primarily serve notice to the LogUI client that any `events` have been successfully saved, and can be disposed of by the LogUI client. 243 | 244 | ```json 245 | { 246 | "messageType": "logui-application-specific-data-saved" 247 | } 248 | ``` 249 | 250 | If for any reason the request failed, a failure response is issued. 251 | 252 | ### WebSocket Disconnection 253 | 254 | A WebSocket disconnect can occur on the server-side or the client-side. We take each scenario in turn. By far the most likely occurrence will be a client-side disconnection. 255 | 256 | #### Client-Side Disconnection 257 | 258 | Client-side disconnections can happen for four main reasons. 259 | 260 | 1. The user moves away from the page being logged by LogUI, causing the LogUI client library to be unloaded. 261 | 2. The code controlling the LogUI client programmatically instructs the LogUI client to stop. 262 | 3. The user's Internet connection is interrupted. 263 | 4. The user's browser and/or computer crashes. 264 | 265 | Not much can be done to recover from the fourth reason. For the third reason, the process is [outlined in this section](#attempting-to-reconnect). However, for the first two reasons, the LogUI client needs to send to the LogUI server any events that it has saved before unloading. 266 | 267 | In this eventuality, the LogUI client is expected to send the following request to the LogUI server. 268 | 269 | ```json 270 | { 271 | "messageType": "logui-client-shutdown", 272 | "clientShutdownTimestamp": "641143800", 273 | "saveEvents": { 274 | "messageType": "logui-event-payload", 275 | "events": [ 276 | { 277 | "timestamp": "123456789", 278 | "eventName": "click" 279 | }, 280 | ... 281 | ] 282 | } 283 | } 284 | ``` 285 | 286 | The `logui-client-shutdown` request includes a `saveEvents` field, which is itself an encapsulated `logui-event-payload` request. Zero or more `events` can be sent with this request. The `logui-client-shutdown` request also includes a `clientShutdownTimestamp` field, the value of which is the UNIX timestamp (from the client's clock) for the point at which the request is sent. 287 | 288 | Upon the receipt of this request, the LogUI server will simply close the connection to the LogUI client. This is considered acknowledgement that the events have been successfully saved, and all loose ends on the LogUI server have been cleared up. 289 | 290 | #### Server-Side Disconnection 291 | 292 | WebSocket disconnections are initiated by the LogUI server when it is about to be shut down, or dies. Where possible, the LogUI server will send a message alerting any LogUI clients connected to it of the impending shutdown. 293 | 294 | ```json 295 | { 296 | "messageType": "logui-server-shutdown-alert" 297 | } 298 | ``` 299 | 300 | This simple message is then expected to be followed up by a response from the LogUI client. As the server is about to go down, all events that the LogUI client presently has stored need to be flushed to the server. The client should respond in a timely manner with the following request. 301 | 302 | ```json 303 | { 304 | "messageType": "logui-server-shutdown-acknowledge", 305 | "clientShutdownTimestamp": "641143800", 306 | "saveEvents": { 307 | "messageType": "logui-event-payload", 308 | "events": [ 309 | { 310 | "timestamp": "123456789", 311 | "eventName": "click" 312 | }, 313 | ... 314 | ] 315 | } 316 | } 317 | ``` 318 | 319 | This request contains a packaged version of a `logui-event-payload` request (via the `saveEvents` field), containing all of the events that the LogUI client requests to be saved. The request also includes a `clientShutdownTimestamp` field, which represents the UNIX timestamp (as per the client's clock) at the point when the request is sent. If the server is still active, the server will save the events, and respond with the following message. 320 | 321 | ```json 322 | { 323 | "messageType": "logui-server-shutdown-saved" 324 | } 325 | ``` 326 | 327 | After this has been received, the LogUI client should expect the LogUI server to close the WebSocket connection. 328 | 329 | The `logui-server-shutdown-saved` message serves as confirmation to the LogUI client that the events that were passed were passed as part of the `logui-server-shutdown-acknowledge` request were successfully saved, and can be discarded by the LogUI client. 330 | 331 | In the eventuality that the WebSocket connection is closed before receiving this acknowledgement message, the LogUI client **should not assume that the events have been saved.** They should be retained by the client as it attempts to reconnect to the LogUI server. 332 | 333 | #### Attempting to Reconnect 334 | 335 | If the WebSocket connection was lost (either through the server closing the connection, or through some connection loss), the LogUI client should then continue to log events, storing them client-side temporarily. While the user continues to interact with the page being logged, the LogUI client should attempt to reconnect to the LogUI server every 30 seconds. 336 | 337 | If a WebSocket connection is re-established, the LogUI client should undertake the [handshake process](#logui-handshake) once more, taking care to include the `sessionUUID` field within the handshake to ensure continuity with the session being tracked. Immediately after a successful handshake, the LogUI client should send a `logui-event-payload` request to the LogUI server, flushing the buildup of logged events on the client. After this has been acknowledged by the LogUI server, normality can resume. 338 | 339 | As mentioned, the LogUI client will continue to log events while the LogUI server is down. However, there is obviously a limit to how much can be stored in the client's memory. This will be measured by the LogUI client, and if a sensible limit is reached before reconnecting to the LogUI server, the LogUI client will have to shut down. 340 | 341 | If reconnection to the LogUI server cannot be made before the user leaves the page that they are on, the saved event data is unfortunately lost as there is no way to save it. 342 | 343 | *When the browser is closed, all context is lost. If the user closes their browser tab or window, capturing the data when offline becomes an impossible task.* 344 | 345 | ## Dealing with Bad Requests 346 | 347 | If the LogUI client sends a request that the LogUI server does not understand, this is considered to be a **bad request**. A bad request can only occur in the **Event Listening** or **WebSocket Disconnection** stages of the LogUI protocol. If a bad request is sent during the LogUI Handshake stage, this is considered to be a `logui-handshake-failure`. Otherwise, the `messageType` will be a `logui-bad-request`. 348 | 349 | Refer to the [following section](#logui-failure-responses) for the possible error messages that can be sent back to the LogUI client in the case of failure. 350 | 351 | A total of five bad requests can be made before the server disconnects the LogUI client. After the fifth bad request is made, the LogUI server simply closes the WebSocket connection without any acknowledgement. 352 | 353 | ## LogUI Failure Responses 354 | 355 | If a request sent to the LogUI server results in some kind of failure, the specific failure type is sent back via `messageType`, with details specific to the failure reported in the `errorDetails` field. This contains at least two subfields: 356 | 357 | - `errorCode`, reporting a specific failure code (unique to the type of failure); and 358 | - `terminateConnection`, a boolean indicating whether the failure is severe enough to warrant a closing of the WebSocket. 359 | 360 | Some failures cannot be recovered from. For example, a `logui-handshake-failure` denotes that the handshake failed. With the authentication process not complete, the LogUI server will not take loggable events. Thus, the WebSocket connection is no longer required. See the example below. 361 | 362 | ```json 363 | { 364 | "messageType": "logui-handshake-failure", 365 | "failureDetails": { 366 | "failureCode": 10, 367 | "terminateConnection": true 368 | } 369 | } 370 | ``` 371 | 372 | Some failures can be recovered from! In these scenarios, the server will respond with `terminateConnection` to `false`. If set to `true`, the server will be expected to close the WebSocket connection. The WebSocket connection should only be closed from the LogUI client side when: 373 | 374 | 1) the user instructs their browser to navigate to a different page, thus forcing the LogUI client library to close; or 375 | 2) the developer integrating the LogUI library instructs it to close (via the LogUI client API). 376 | 377 | In some failure scenarios, additional metadata about the failure may be present in the `failureDetails` field. However, one can be assured that the `failureCode` and `terminateConnection` fields will be present in all failure scenarios. 378 | 379 | The following subsections report the possible `failureCode` values possible for each failure type. 380 | 381 | ### `logui-handshake-failure` 382 | 383 | With this type of failure, the LogUI handshake was not successful. In all scenarios, the WebSocket connection will be terminated. Failure codes `100` through `109` are devoted to this failure type. 384 | 385 | - **Code `100` (Generic)** 386 | A generic failure code for `logui-handshake-failure`. Not documented here. 387 | 388 | - **Code `101` (Badly Formatted)** 389 | One or more fields missing from the handshake request. 390 | 391 | - **Code `102` (Bad `applicationIdentifier` String)** 392 | An invalid `applicationIdentifier` string was supplied. Decryption failure. 393 | 394 | - **Code `103` (Unknown Application or Flight ID)** 395 | An unknown `applicationID` or `flightID` were supplied. One did not match against the database. 396 | 397 | - **Code `104` (Version Mismatch)** 398 | A mismatched version was supplied from the `applicationIdentifier` string. A version of the LogUI Client library is being used that does not match with what is expected. 399 | 400 | - **Code `105` (Unsupported Client Version)** 401 | An unsupported LogUI client version is being used with the LogUI server endpoint. 402 | 403 | ### `logui-bad-request` 404 | 405 | A bad request encompasses all eventualities after the handshake stage of the protocol. Failure codes `200` to `209` are devoted to this failure type. 406 | 407 | - **Code `200` (Generic)** 408 | A generic failure code for requests that take place after the handshake stage. Not documented here. 409 | 410 | - **Code `201` (`logui-event-payload` Badly Formed)** 411 | The `logui-event-payload` request is badly formed. Either bad JSON was supplied, or one or more required fields were missing. 412 | 413 | - **Code `202` (`logui-event-payload` Missing Field)** 414 | One of the `logui-event-payload` event fields were missing. Ensure that the required fields are present for each field. 415 | 416 | - **Code `203` (`logui-application-specific-data-change` Badly Formed)** 417 | The `logui-application-specific-data-change` request is badly formed. The request contained badly formed JSON, or was missing one or more required fields. 418 | 419 | ### `logui-server-failure` 420 | 421 | A LogUI server failure will almost always entail that the WebSocket connection will be closed. Failure codes `300` to `309` are devoted to this failure type. 422 | 423 | - **Code `300` (Generic)** 424 | A generic LogUI server failure code for events that are not captured by their own identifying failure code. 425 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LogUI Client 2 | 3 | **Welcome to LogUI!** *LogUI* is a powerful, framework-agnostic client-side JavaScript library that can be used for logging interactions that take place on a webpage. Primarily designed for *Interactive Information Retrieval (IIR)* experiments, LogUI can in theory be used on any page or site that you wish to track fine-grained user interactions with UI components. 4 | 5 | Use the LogUI client in tandem with the LogUI server. You can find the LogUI server living at [this repository](https://github.com/logui-framework/server/). 6 | 7 | ## About LogUI 8 | 9 | The LogUI library is implemented by [Dr David Maxwell](https://github.com/maxwelld90/), a postdoctoral researcher at [TUDelft](https://www.tudelft.nl/) in the Netherlands. It has been developed in the Lambda Lab, headed by [Dr Claudia Hauff](https://chauff.github.io/). The library is borne out of the need for infrastructure that allows one to undertake the logging of user interactions in a consistent way, rather than the piecemeal approach that we've seen in IIR experimentation. 10 | 11 | We think that a one-size-fits-all logging library is just the ticket for your experiments! 12 | 13 | ## Using LogUI in Experiments? 14 | 15 | We're thrilled that you're using LogUI in your experiments! We ask that in return you provide due credit for this work. If you have a paper associated with your experiment, please do cite the associated demonstration paper that was published at [ECIR 2021](https://www.ecir2021.eu/). You can find the BibTeX source for the paper below. 16 | 17 | ```bibtex 18 | @inproceedings{maxwell2021logui, 19 | author = {Maxwell, David and Hauff, Claudia}, 20 | title ="{LogUI: Contemporary Logging Infrastructure for Web-Based Experiments}", 21 | booktitle = {Advances in Information Retrieval (Proc. ECIR)}, 22 | year = {2021}, 23 | pages = {525--530}, 24 | } 25 | ``` 26 | 27 | ## Documentation and Quick Start Guide 28 | 29 | For documentation on the LogUI client library, please go and check the corresponding Wiki associated with this repository. There, you'll find detailed information about how to [acquire yourself a copy](https://github.com/logui-framework/client/wiki/Acquiring), how to [set the client library up](https://github.com/logui-framework/client/wiki/Quick-Start-Guide), how to integrate it with your existing application's code, and information which should allow you to gain a better understanding as to the thinking behind the library's implementation. 30 | 31 | ## Tests 32 | 33 | Tests are being developed for the LogUI client library and will be available in this repository soon. 34 | 35 | ## Found a Bug or have a Feature Request? 36 | 37 | It would be great to hear from you! Please [raise an issue in this repository](https://github.com/logui-framework/client/issues) and we can discuss what options that can be pursued to resolve it. -------------------------------------------------------------------------------- /build/env/controller.js: -------------------------------------------------------------------------------- 1 | var LogUITestEnvDriver = (function(root) { 2 | var _public = {}; 3 | var initTimestamp = null; 4 | var detectReference = null; 5 | 6 | _public.$ = root.document.querySelector.bind(root.document); 7 | _public.$$ = root.document.querySelectorAll.bind(root.document); 8 | 9 | const CONSOLE_LIST_ELEMENT = _public.$('#console-list'); 10 | 11 | const STATUS_MESSAGES = { 12 | 'unloaded': 'LogUI unloaded', 13 | 'inactive': 'LogUI loaded; inactive', 14 | 'starting': 'LogUI loaded; starting...', 15 | 'stopping': 'LogUI loaded; stopping...', 16 | 'active': 'LogUI loaded; active' 17 | } 18 | 19 | _public.init = function() { 20 | initTimestamp = new Date(); 21 | _public.clearConsole(); 22 | _public.addEnvMessage('Initialising test environment'); 23 | _public.addEnvMessage('Messages with a blue background (like this) are from the test environment.'); 24 | setStatus('unloaded'); 25 | 26 | detectReference = window.setInterval(detectLogUI, 500); 27 | bindButtonListeners(); 28 | }; 29 | 30 | _public.addEnvMessage = function(msg) { 31 | let newNode = document.createElement('li'); 32 | let textNode = document.createTextNode(msg); 33 | newNode.appendChild(textNode); 34 | newNode.classList.add('env'); 35 | 36 | CONSOLE_LIST_ELEMENT.insertBefore(newNode, CONSOLE_LIST_ELEMENT.firstChild); 37 | }; 38 | 39 | _public.clearConsole = function() { 40 | CONSOLE_LIST_ELEMENT.innerHTML = ''; 41 | }; 42 | 43 | function bindButtonListeners() { 44 | _public.$('#control-clear').addEventListener('click', function() { 45 | _public.clearConsole(); 46 | }); 47 | 48 | _public.$('#control-start').addEventListener('click', function() { 49 | _public.addEnvMessage('Starting LogUI'); 50 | setStatus('starting'); 51 | _public.$('#control-start').disabled = true; 52 | 53 | window.LogUI.init(window.config) 54 | .catch(error => {throw Error(error)}); 55 | }); 56 | 57 | _public.$('#control-stop').addEventListener('click', function() { 58 | _public.addEnvMessage('Stopping LogUI'); 59 | setStatus('stopping'); 60 | 61 | _public.$('#control-stop').disabled = true; 62 | 63 | window.LogUI.stop().then(function(resolved) { 64 | _public.$('#control-start').disabled = false; 65 | setStatus('inactive'); 66 | }); 67 | }); 68 | 69 | root.addEventListener('logUIStarted', function() { 70 | if (window.LogUI.isActive()) { 71 | _public.$('#control-stop').disabled = false; 72 | setStatus('active'); 73 | _public.addEnvMessage('LogUI started; listening for events'); 74 | } 75 | }); 76 | 77 | // This listener is bound to demonstrate its functionality. 78 | // We could equally put this code in the .stop().then() call above. 79 | root.addEventListener('logUIStopped', function() { 80 | _public.addEnvMessage('LogUI stopped'); 81 | 82 | _public.$('#control-start').disabled = false; 83 | _public.$('#control-stop').disabled = true; 84 | }); 85 | }; 86 | 87 | function setStatus(statusKey) { 88 | _public.$('#control-status').innerText = STATUS_MESSAGES[statusKey]; 89 | 90 | if (statusKey == 'inactive') { 91 | _public.$('#control-version').style.display = 'inline'; 92 | _public.$('#control-version').innerText = `Version ${LogUI.buildVersion}`; 93 | } 94 | }; 95 | 96 | function detectLogUI() { 97 | if (window.LogUI) { 98 | window.clearInterval(detectReference); 99 | setStatus('inactive'); 100 | _public.$('#control-start').disabled = false; 101 | } 102 | }; 103 | 104 | return _public; 105 | })(window); 106 | 107 | document.addEventListener('DOMContentLoaded', function() { 108 | LogUITestEnvDriver.init(); 109 | 110 | LogUITestEnvDriver.$('#test-dommanipulation-button').addEventListener('click', function() { 111 | if (this.innerHTML.includes('add')) { 112 | var element1 = document.createElement('div'); 113 | element1.appendChild(document.createTextNode('No binding')) 114 | element1.id = 'test-dommanipulation-box1'; 115 | element1.classList.add('test'); 116 | 117 | var element2 = document.createElement('div'); 118 | element2.appendChild(document.createTextNode('Hover and click binding')) 119 | element2.id = 'test-dommanipulation-box2'; 120 | element2.classList.add('test'); 121 | 122 | LogUITestEnvDriver.$('#test-dommanipulation-newcontainer').appendChild(element1); 123 | LogUITestEnvDriver.$('#test-dommanipulation-newcontainer').appendChild(element2); 124 | 125 | this.innerHTML = 'Click to destroy elements'; 126 | 127 | return; 128 | } 129 | 130 | this.innerHTML = 'Click to add two new elements'; 131 | LogUITestEnvDriver.$('#test-dommanipulation-newcontainer').innerHTML = ''; 132 | }); 133 | }); -------------------------------------------------------------------------------- /build/env/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | LogUI Test Environment 10 | 11 | 12 | 13 |

14 | LogUI Test Environment 15 | 16 |

17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | JavaScript disabled! 25 | 26 |
27 | 28 |
29 |

This is a simple LogUI test environment. Use this with a build of LogUI and the console dispatcher (npm run -s build:console) to have LogUI drive the console at the bottom of the screen.

30 |

Control LogUI with the buttons above, and interact with the sample elements below. Interact with the page, too. Try resizing the viewport, or scrolling with your mouse wheel or trackpad. Watch the corresponding log events appear in the console.

31 |

Note that the events that appear in the console below are the log events that would otherwise be directed to the LogUI server. Check out the browser's console for verbose output on what is going on in terms of initialisation (if verbose: true).

32 | 33 |
34 | 35 |
36 |
37 |

Basic Hover/Click Events

38 |
39 |
Hover!
40 |
41 |
Nested
42 |
43 |
44 |
45 |
46 |

Dynamic DOM Manipulation

47 |
48 | 49 |
50 |
51 |
52 |
53 |

Basic React Application

54 |
55 | The app is below the horizontal rule. 56 |
57 |
58 |
59 |
60 |

List of Elements

61 |
62 |
    63 |
  • List item 1 Nested!
  • 64 |
  • List item 2 Nested!
  • 65 |
  • List item 3 Nested!
  • 66 |
67 |
68 |
69 |
70 |

List of Elements (Convoluted)

71 |
72 |
73 |
74 |
75 | 76 | List item one 77 | 78 |
79 |
80 | 81 | List item two 82 | 83 |
84 |
85 | 86 | List item three 87 | 88 |
89 |
90 | 91 | List item four 92 | 93 |
94 |
95 |
96 |
97 |
98 |
99 |

Scrollable Element

100 | 101 |
102 | This is a scrollable element. You should be able to scroll along the y axis to see more content. Look at the interaction logs to see the scroll events that are recorded. We should see a start and stop scroll event; not a multitude of scroll events that are recorded. 103 |































104 |
105 |
106 |
107 |

Mouse Clicks

108 | 109 |
Left Click
110 |
Centre Click
111 |
Right Click
112 |
113 |
114 | 115 |
116 | 117 |
118 | 119 |
120 | 121 | 122 | 123 | 124 | 125 | 272 | -------------------------------------------------------------------------------- /build/env/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none} -------------------------------------------------------------------------------- /build/env/styles.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif; 4 | line-height: 1.5; 5 | } 6 | 7 | hr { 8 | border: 1px solid #333; 9 | } 10 | 11 | h1 { 12 | margin: 0 auto 0 auto; 13 | width: 95%; 14 | padding-top: 15px; 15 | line-height: 1.2; 16 | } 17 | 18 | h1 img { 19 | float: right; 20 | width: 100px; 21 | } 22 | 23 | div#controls { 24 | margin: 25px auto 0 auto; 25 | width: 95%; 26 | padding: 10px; 27 | -webkit-box-sizing: border-box; 28 | -moz-box-sizing: border-box; 29 | box-sizing: border-box; 30 | background-color: #4A4A4A; 31 | color: #FFF; 32 | border-radius: 5px; 33 | } 34 | 35 | div#controls span#control-container { 36 | float: right; 37 | } 38 | 39 | div#controls span#control-container span#control-status { 40 | font-weight: bold; 41 | } 42 | 43 | div#controls span#control-container span#control-version { 44 | display: none; 45 | padding: 3px; 46 | margin-right: 5px; 47 | font-size: 10pt; 48 | background-color: rgba(255, 255, 255, 0.2); 49 | border-radius: 5px; 50 | } 51 | 52 | main { 53 | margin: 0 auto 0 auto; 54 | width: 95%; 55 | min-height: 3000px; 56 | padding-top: 15px; 57 | } 58 | 59 | main div#elements-container { 60 | position: relative; 61 | column-count: 3; 62 | column-gap: 1em; 63 | column-fill: auto; 64 | -webkit-column-fill: auto; 65 | -moz-column-fill: auto; 66 | -ms-column-fill: auto; 67 | -o-column-fill: auto; 68 | margin-top: 20px; 69 | padding: 10px; 70 | background-color: #EFEFEF; 71 | border-radius: 5px; 72 | margin-bottom: 35%; 73 | } 74 | 75 | main div#elements-container h2 { 76 | display: inline-block; 77 | font-size: 14pt; 78 | margin: 0 0 10px 0; 79 | padding: 0; 80 | border-bottom: 1px solid #4A4A4A; 81 | } 82 | 83 | main div#elements-container div.group-container { 84 | position: relative; 85 | margin-bottom: 20px; 86 | padding: 10px; 87 | background: #C6C6C6; 88 | border-radius: 5px; 89 | break-inside: avoid-column; 90 | } 91 | 92 | div#console { 93 | position: fixed; 94 | left: 0; 95 | bottom: 0; 96 | width: 100%; 97 | height: 40%; 98 | background-color: #212121; 99 | border-top: 1px solid #333; 100 | color: #FFF; 101 | font-family: 'Monaco', 'Menlo', 'Courier New', monospace; 102 | font-size: 10pt; 103 | line-height: 1.6; 104 | overflow-y: auto; 105 | } 106 | 107 | div#console ul#console-list { 108 | margin: 0 auto 0 auto; 109 | padding: 10px 0 0 0; 110 | width: 95%; 111 | } 112 | 113 | div#console ul#console-list li { 114 | margin: 0; 115 | list-style-type: none; 116 | padding: 5px 10px 5px 10px; 117 | border-bottom: 1px solid #4A4A4A; 118 | } 119 | 120 | div#console ul#console-list li.env { 121 | background-color: rgba(0, 114, 198, 0.5); 122 | } 123 | 124 | /* Styles for individual test components */ 125 | .test { 126 | -webkit-box-sizing: border-box; 127 | -moz-box-sizing: border-box; 128 | padding: 10px; 129 | } 130 | 131 | main div#elements-container div.group-container#container-test-click { 132 | min-height: 300px; 133 | } 134 | 135 | div#test-click-box1 { 136 | width: 100px; 137 | height: 100px; 138 | background-color: aquamarine; 139 | } 140 | 141 | div#test-click-box2 { 142 | position: absolute; 143 | bottom: 10px; 144 | right: 10px; 145 | width: 50%; 146 | height: 40%; 147 | background-color: #3366FF; 148 | } 149 | 150 | div#test-click-box3 { 151 | width: 50%; 152 | height: 75%; 153 | float: right; 154 | background-color: #FF6600; 155 | } 156 | 157 | main div#elements-container div.group-container#container-test-dommanipulation { 158 | min-height: 200px; 159 | } 160 | 161 | div#test-dommanipulation-box1 { 162 | position: relative; 163 | top: 15px; 164 | width: 100px; 165 | height: 100px; 166 | float: left; 167 | background-color: #F00; 168 | } 169 | 170 | div#test-dommanipulation-box2 { 171 | position: relative; 172 | top: 15px; 173 | width: 160px; 174 | height: 120px; 175 | float: right; 176 | background-color: seagreen; 177 | } 178 | 179 | div#test-reactapp-clock { 180 | text-align: center; 181 | font-family: monospace; 182 | font-size: 32pt; 183 | font-weight: bold; 184 | background-color: #666; 185 | color: #FFF; 186 | border-radius: 5px; 187 | margin-bottom: 5px; 188 | } 189 | 190 | ul#listofelements li { 191 | background-color: grey; 192 | margin-bottom: 5px; 193 | } 194 | 195 | ul#listofelements li span { 196 | background-color: red; 197 | } 198 | 199 | div#scrollable-element { 200 | background-color: #999; 201 | box-sizing: border-box; 202 | padding: 10px; 203 | height: 100px; 204 | overflow-y: scroll; 205 | } 206 | 207 | div#left-click-box { 208 | width: 90px; 209 | height: 90px; 210 | background-color:red; 211 | color: white; 212 | margin-bottom: 10px; 213 | } 214 | 215 | div#centre-click-box { 216 | width: 90px; 217 | height: 90px; 218 | position: relative; 219 | left: 100px; 220 | background-color:yellow; 221 | margin-bottom: 10px; 222 | } 223 | 224 | div#right-click-box { 225 | width: 90px; 226 | height: 90px; 227 | position: relative; 228 | left: 200px; 229 | background-color:green; 230 | color: white; 231 | margin-bottom: 10px; 232 | } -------------------------------------------------------------------------------- /build/reactapp/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react", 5 | ], 6 | 7 | "plugins": [ 8 | [ 9 | "@babel/plugin-proposal-class-properties", 10 | { 11 | "loose": true 12 | } 13 | ], 14 | [ 15 | "@babel/plugin-transform-classes", 16 | { 17 | "loose": true 18 | } 19 | ], 20 | [ 21 | "@babel/plugin-transform-runtime", 22 | { 23 | "regenerator": true 24 | } 25 | ] 26 | ] 27 | } -------------------------------------------------------------------------------- /build/reactapp/app/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class Clock extends React.Component { 4 | 5 | constructor(props) { 6 | super(props); 7 | 8 | this.state = { 9 | time: new Date().toISOString().substr(11, 8), 10 | }; 11 | 12 | this.clockTick = this.clockTick.bind(this); 13 | } 14 | 15 | componentDidMount() { 16 | this.tick = setInterval( 17 | this.clockTick, 18 | 1000 19 | ); 20 | } 21 | 22 | componentWillUnmount() { 23 | clearInterval(this.tick); 24 | } 25 | 26 | clockTick() { 27 | this.setState({ 28 | time: new Date().toISOString().substr(11, 8) 29 | }); 30 | } 31 | 32 | render() { 33 | return ( 34 |
35 | {this.state.time} 36 |
); 37 | } 38 | 39 | } 40 | 41 | class RootComponent extends React.Component { 42 | 43 | render() { 44 | return ( 45 |
46 |
47 | 48 | Hover over the ticking clock to log the Clock state in a mouseover event. 49 |
50 | ); 51 | } 52 | 53 | } 54 | 55 | export default RootComponent; -------------------------------------------------------------------------------- /build/reactapp/app/root.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import AppRoot from './app'; 4 | 5 | ReactDOM.render( 6 | 7 | 8 | , 9 | document.getElementById('test-reactapp-container') 10 | ); -------------------------------------------------------------------------------- /build/reactapp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "build": "browserify ./app/root.js -t babelify --outfile ../reactapp.bundle.js" 4 | }, 5 | "devDependencies": { 6 | "@babel/core": "^7.12.9", 7 | "@babel/plugin-proposal-class-properties": "^7.12.1", 8 | "@babel/plugin-transform-classes": "^7.12.1", 9 | "@babel/preset-env": "^7.12.7", 10 | "@babel/preset-react": "^7.12.7", 11 | "babelify": "^10.0.0", 12 | "react": "^17.0.1", 13 | "react-dom": "^17.0.1" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "logui", 3 | "version": "0.5.4a", 4 | "description": "A framework-agnostic client-side JavaScript library for logging user interactions on webpages.", 5 | "main": "./src/main.js", 6 | "scripts": { 7 | "build": "cross-env NODE_ENV=production rollup -c", 8 | "build:nouglify": "cross-env NODE_ENV=production NOUGLIFY=true rollup -c", 9 | "build:nouglify:console": "cross-env NODE_ENV=production NOUGLIFY=true DISPATCHER=console rollup -c", 10 | "build:console": "cross-env NODE_ENV=production DISPATCHER=console rollup -c", 11 | "test": "npm run -s test:build && npm run -s test:run", 12 | "test:build": "echo '> Building test bundle...' && cross-env NODE-ENV=test rollup -c", 13 | "test:run": "echo '> Running tests...' && mocha ./tests/modules/*.js --timeout 5000" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/maxwelld90/logui-client.git" 18 | }, 19 | "keywords": [ 20 | "javascript", 21 | "logging", 22 | "logging-client", 23 | "framework-agnostic", 24 | "client-side" 25 | ], 26 | "author": { 27 | "name": "David Maxwell", 28 | "email": "maxwelld90@acm.org", 29 | "url": "https://www.dmax.org.uk" 30 | }, 31 | "bugs": { 32 | "url": "https://github.com/maxwelld90/logui-client/issues" 33 | }, 34 | "homepage": "https://github.com/maxwelld90/logui-client#readme", 35 | "files": [ 36 | "README.md", 37 | "CHANGELOG.md" 38 | ], 39 | "directories": { 40 | "test": "tests/modules/" 41 | }, 42 | "devDependencies": { 43 | "@babel/core": "^7.11.6", 44 | "@babel/plugin-transform-runtime": "^7.11.5", 45 | "@babel/preset-env": "^7.11.5", 46 | "@rollup/plugin-replace": "^2.3.3", 47 | "babel-plugin-import-directory": "^1.1.0", 48 | "chai": "^4.2.0", 49 | "cross-env": "^7.0.2", 50 | "http-server": "^0.12.3", 51 | "jsonschema": "^1.2.6", 52 | "mocha": "^8.1.3", 53 | "playwright": "^1.4.0", 54 | "rollup": "^2.26.11", 55 | "rollup-plugin-babel": "^4.4.0", 56 | "rollup-plugin-commonjs": "^10.1.0", 57 | "rollup-plugin-node-builtins": "^2.1.2", 58 | "rollup-plugin-node-resolve": "^5.2.0", 59 | "rollup-plugin-uglify": "^6.0.4", 60 | "specificity": "^0.4.1" 61 | }, 62 | "dependencies": {} 63 | } 64 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | import resolve from 'rollup-plugin-node-resolve'; 3 | import commonjs from 'rollup-plugin-commonjs'; 4 | import replace from '@rollup/plugin-replace'; 5 | import { uglify } from 'rollup-plugin-uglify'; 6 | import builtins from 'rollup-plugin-node-builtins'; 7 | 8 | const packageProps = require('./package.json'); 9 | const isProduction = (process.env.NODE_ENV === 'production') ? true : false; 10 | const buildEnvironment = (process.env.NODE_ENV === 'production') ? 'production' : 'test'; 11 | const uglifyBuild = (process.env.NOUGLIFY == 'true') ? false : true; 12 | let dispatcherImport = './modules/dispatchers/websocketDispatcher'; 13 | let dispatcherImportInPackager = './dispatchers/websocketDispatcher'; 14 | 15 | switch (process.env.DISPATCHER) { 16 | case 'console': 17 | dispatcherImport = './modules/dispatchers/consoleDispatcher'; 18 | dispatcherImportInPackager = './dispatchers/consoleDispatcher'; 19 | break; 20 | } 21 | 22 | export default { 23 | input: ['./src/main.js'], 24 | output: { 25 | ...(isProduction ? {file: './build/logui.bundle.js'} : {file: './tests/logui.test.bundle.js'}), 26 | format: 'iife', 27 | name: 'LogUI' 28 | }, 29 | plugins: [ 30 | ...uglifyBuild ? [ 31 | replace({ 32 | __isProduction__: isProduction, 33 | __buildDate__: () => new Date(), 34 | __buildVersion__: packageProps.version, 35 | __buildEnvironment__: buildEnvironment, 36 | __dispatcherImport__: dispatcherImport, 37 | __dispatcherImportInPackager__: dispatcherImportInPackager, 38 | }), 39 | builtins(), 40 | resolve(), 41 | commonjs(), 42 | babel({ 43 | runtimeHelpers: true 44 | }), 45 | uglify(), 46 | ]: [ 47 | replace({ 48 | __isProduction__: isProduction, 49 | __buildDate__: () => new Date(), 50 | __buildVersion__: packageProps.version, 51 | __buildEnvironment__: buildEnvironment, 52 | __dispatcherImport__: dispatcherImport, 53 | __dispatcherImportInPackager__: dispatcherImportInPackager, 54 | }), 55 | builtins(), 56 | resolve(), 57 | commonjs(), 58 | babel({ 59 | runtimeHelpers: true 60 | }), 61 | ] 62 | ] 63 | } -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Config from './modules/config'; 2 | import Dispatcher from '__dispatcherImport__'; 3 | import DOMHandler from './modules/DOMHandler/handler'; 4 | import EventPackager from './modules/eventPackager'; 5 | import MetadataHandler from './modules/metadataHandler'; 6 | import SpecificFrameworkEvents from './modules/specificFrameworkEvents'; 7 | import EventHandlerController from './modules/eventHandlerController'; 8 | 9 | export default (function(root) { 10 | var _public = {}; 11 | 12 | /* Public build variables */ 13 | _public.buildVersion = '__buildVersion__'; 14 | _public.buildEnvironment = '__buildEnvironment__'; 15 | _public.buildDate = '__buildDate__'; 16 | 17 | _public.Config = Config; 18 | 19 | /* API calls */ 20 | _public.init = async function(suppliedConfigObject) { 21 | root.addEventListener('logUIShutdownRequest', _public.stop); 22 | 23 | if (!suppliedConfigObject) { 24 | throw Error('LogUI requires a configuration object to be passed to the init() function.'); 25 | } 26 | 27 | if (!Config.init(suppliedConfigObject)) { 28 | throw Error('The LogUI configuration component failed to initialise. Check console warnings to see what went wrong.'); 29 | } 30 | 31 | if (!MetadataHandler.init()) { 32 | throw Error('The LogUI metadata handler component failed to initialise. Check console warnings to see what went wrong.'); 33 | } 34 | 35 | if (!EventPackager.init()) { 36 | throw Error('The LogUI event packaging component failed to initialise. Check console warnings to see what went wrong.'); 37 | } 38 | 39 | if (!SpecificFrameworkEvents.init()) { 40 | throw Error('The LogUI events component failed to initialise. Check console warnings to see what went wrong.'); 41 | } 42 | 43 | if (!await Dispatcher.init(suppliedConfigObject)) { 44 | throw Error('The LogUI dispatcher component failed to initialise. Check console warnings to see what went wrong.'); 45 | } 46 | 47 | if (!DOMHandler.init()) { 48 | throw Error('The LogUI DOMHandler component failed to initialise. Check console warnings to see what went wrong.'); 49 | } 50 | 51 | if (!EventHandlerController.init()) { 52 | throw Error('The LogUI event handler controller component failed to initialise. Check console warnings to see what went wrong.'); 53 | } 54 | 55 | root.addEventListener('unload', _public.stop); 56 | }; 57 | 58 | _public.isActive = function() { 59 | return ( 60 | Config.isActive() && 61 | Dispatcher.isActive()); 62 | } 63 | 64 | _public.stop = async function() { 65 | if (!_public.isActive()) { 66 | throw Error('LogUI may only be stopped if it is currently running.'); 67 | } 68 | 69 | root.removeEventListener('unload', _public.stop); 70 | root.removeEventListener('logUIShutdownRequest', _public.stop); 71 | 72 | // https://stackoverflow.com/questions/42304996/javascript-using-promises-on-websocket 73 | DOMHandler.stop(); 74 | EventHandlerController.stop(); 75 | SpecificFrameworkEvents.stop(); 76 | EventPackager.stop(); 77 | MetadataHandler.stop(); 78 | await Dispatcher.stop(); 79 | Config.reset(); 80 | root.dispatchEvent(new Event('logUIStopped')); 81 | }; 82 | 83 | _public.logCustomMessage = function(messageObject) { 84 | if (!_public.isActive()) { 85 | throw Error('Custom messages may only be logged when the LogUI client is active.'); 86 | } 87 | 88 | EventPackager.packageCustomEvent(messageObject); 89 | }; 90 | 91 | _public.updateApplicationSpecificData = function(updatedObject) { 92 | if (!_public.isActive()) { 93 | throw Error('Application specific data can only be updated when the LogUI client is active.'); 94 | } 95 | 96 | Config.applicationSpecificData.update(updatedObject); 97 | SpecificFrameworkEvents.logUIUpdatedApplicationSpecificData(); 98 | }; 99 | 100 | _public.deleteApplicationSpecificDataKey = function(key) { 101 | Config.applicationSpecificData.deleteKey(key); 102 | SpecificFrameworkEvents.logUIUpdatedApplicationSpecificData(); 103 | } 104 | 105 | _public.clearSessionID = function() { 106 | if (_public.isActive()) { 107 | throw Error('The session ID can only be reset when the LogUI client is inactive.'); 108 | } 109 | 110 | Config.sessionData.clearSessionIDKey(); 111 | }; 112 | 113 | return _public; 114 | })(window); -------------------------------------------------------------------------------- /src/modules/DOMHandler/DOMPropertiesObject.js: -------------------------------------------------------------------------------- 1 | /* 2 | LogUI Client Library 3 | DOM Properties Object Factory Module 4 | 5 | IIFE function that provides a factory for DOM Properties objects, which are stored in the Config WeakMap. 6 | 7 | @module: DOM Properties Object Factory 8 | @author: David Maxwell 9 | @date: 2020-03-02 10 | */ 11 | 12 | export default (function(root) { 13 | 14 | var createObject = function(groupObject) { 15 | let newDOMPropertiesObject = { 16 | events: {}, 17 | 18 | hasEvent: function(eventName) { 19 | if (newDOMPropertiesObject['events'].hasOwnProperty(eventName)) { 20 | return true; 21 | } 22 | 23 | return false; 24 | }, 25 | 26 | getEventGroupName: function(eventName) { 27 | if (newDOMPropertiesObject.hasEvent(eventName)) { 28 | return newDOMPropertiesObject['events'][eventName]; 29 | } 30 | 31 | return false; 32 | }, 33 | 34 | getEventList: function*() { 35 | for (let eventName in newDOMPropertiesObject['events']) { 36 | yield eventName; 37 | } 38 | }, 39 | 40 | deleteEventsWithGroup: function(groupName) { 41 | for (let event in newDOMPropertiesObject['events']) { 42 | if (newDOMPropertiesObject.getEventGroupName(event) == groupName) { 43 | newDOMPropertiesObject.deleteEvent(event); 44 | } 45 | } 46 | }, 47 | 48 | deleteEvent: function(eventName) { 49 | delete newDOMPropertiesObject['events'][eventName]; 50 | }, 51 | 52 | setEvent: function(eventName, groupName) { 53 | newDOMPropertiesObject['events'][eventName] = groupName; 54 | }, 55 | }; 56 | 57 | for (let event of groupObject.eventsList) { 58 | newDOMPropertiesObject['events'][event] = groupObject.name; 59 | } 60 | 61 | return newDOMPropertiesObject; 62 | }; 63 | 64 | return createObject; 65 | })(window); -------------------------------------------------------------------------------- /src/modules/DOMHandler/binder.js: -------------------------------------------------------------------------------- 1 | /* 2 | LogUI Client Library 3 | DOM Handler Binder Module 4 | 5 | IIFE function that provides functionality to bind event listeners to elements. 6 | 7 | @module: DOM Handler Binder 8 | @author: David Maxwell 9 | @date: 2020-03-02 10 | */ 11 | 12 | import Config from '../config'; 13 | import DOMHandlerHelpers from './helpers'; 14 | import EventCallbackHandler from '../eventCallbackHandler'; 15 | 16 | export default (function(root) { 17 | var _public = {}; 18 | 19 | _public.init = function() { 20 | for (let element of DOMHandlerHelpers.generators.uniqueElements()) { 21 | _public.bind(element); 22 | } 23 | }; 24 | 25 | _public.stop = function() { 26 | for (let element of DOMHandlerHelpers.generators.uniqueElements()) { 27 | _public.unbind(element); 28 | } 29 | }; 30 | 31 | _public.bind = function(element) { 32 | let elementDOMProperties = Config.DOMProperties.get(element); 33 | 34 | for (let eventName of elementDOMProperties.getEventList()) { 35 | element.addEventListener(eventName, EventCallbackHandler.logUIEventCallback); 36 | } 37 | }; 38 | 39 | _public.unbind = function(element) { 40 | let elementDOMProperties = Config.DOMProperties.get(element); 41 | 42 | for (let eventName of elementDOMProperties.getEventList()) { 43 | element.removeEventListener(eventName, EventCallbackHandler.logUIEventCallback); 44 | } 45 | }; 46 | 47 | return _public; 48 | })(window); -------------------------------------------------------------------------------- /src/modules/DOMHandler/browserEventsController.js: -------------------------------------------------------------------------------- 1 | /* 2 | LogUI Client Library 3 | Browser Events Controller Module 4 | 5 | IIFE function that provides a controller for maintaining browser events (i.e., not pertaining to a specific page element, rather document or window). 6 | Provides functionality to spin up and stop event listeners. 7 | 8 | @module: Browser Events Controller Module 9 | @author: David Maxwell 10 | @date: 2020-03-02 11 | */ 12 | 13 | import Config from '../config'; 14 | import Helpers from './../helpers'; 15 | import BrowserEvents from './../browserEvents/*'; 16 | 17 | export default (function(root) { 18 | var _public = {}; 19 | 20 | _public.init = function() { 21 | for (let browserEventName in BrowserEvents) { 22 | BrowserEvents[browserEventName].init(); 23 | } 24 | } 25 | 26 | _public.stop = function() { 27 | for (let browserEventName in BrowserEvents) { 28 | let browserEvent = BrowserEvents[browserEventName]; 29 | 30 | if (browserEvent.hasOwnProperty('stop')) { 31 | browserEvent.stop(); 32 | } 33 | } 34 | } 35 | 36 | return _public; 37 | })(window); -------------------------------------------------------------------------------- /src/modules/DOMHandler/handler.js: -------------------------------------------------------------------------------- 1 | /* 2 | LogUI Client Library 3 | DOM Handler Module 4 | 5 | An IIFE function containing key functionality for traversing the DOM, and binding event listeners to elements within the page. 6 | 7 | @module: DOM Handler 8 | @author: David Maxwell 9 | @date: 2020-03-02 10 | */ 11 | 12 | import Binder from './binder'; 13 | import DOMHandlerHelpers from './helpers'; 14 | import BrowserEventsController from './browserEventsController'; 15 | import MutationObserverController from './mutationObserverController'; 16 | 17 | export default (function(root) { 18 | var _public = {}; 19 | 20 | _public.init = function() { 21 | runElementInitialisation(); 22 | BrowserEventsController.init(); 23 | Binder.init(); 24 | MutationObserverController.init(); 25 | 26 | return true; 27 | }; 28 | 29 | _public.stop = function() { 30 | MutationObserverController.stop(); 31 | BrowserEventsController.stop(); 32 | Binder.stop(); 33 | }; 34 | 35 | var runElementInitialisation = function() { 36 | for (let groupObject of DOMHandlerHelpers.generators.trackingConfig()) { 37 | for (let i in Object.keys(groupObject.selectedElements)) { 38 | let element = groupObject.selectedElements[i]; 39 | 40 | DOMHandlerHelpers.processElement(element, groupObject); 41 | 42 | // console.log(element); 43 | // console.log(Config.DOMProperties.get(element)); 44 | // console.log('====='); 45 | } 46 | } 47 | } 48 | 49 | return _public; 50 | })(window); -------------------------------------------------------------------------------- /src/modules/DOMHandler/helpers.js: -------------------------------------------------------------------------------- 1 | /* 2 | LogUI Client Library 3 | DOM Handler Helpers Module 4 | 5 | IIFE function that provides helper functions for DOM traversal, manipulation, and related objects. 6 | 7 | @module: DOM Handler Helpers 8 | @author: David Maxwell 9 | @date: 2020-03-02 10 | */ 11 | 12 | import Config from '../config'; 13 | import Helpers from '../helpers'; 14 | import DOMPropertiesObject from './DOMPropertiesObject'; 15 | import EventHandlerController from '../eventHandlerController'; 16 | import { compare as specificityCompare } from 'specificity'; 17 | 18 | export default (function(root) { 19 | var _public = {}; 20 | 21 | _public.generators = { 22 | 23 | trackingConfig: function*() { 24 | let trackingConfig = Config.elementTrackingConfig.get(); 25 | 26 | for (let groupName in trackingConfig) { 27 | let groupObject = { 28 | name: groupName, 29 | selector: trackingConfig[groupName].selector, 30 | event: trackingConfig[groupName].event, 31 | selectedElements: Helpers.$$(trackingConfig[groupName].selector), 32 | eventsList: getEventsList(trackingConfig[groupName].event), 33 | } 34 | 35 | if (!groupObject.eventsList) { 36 | Helpers.console(`Skipping group '${groupName}'`, 'Initialisation', true); 37 | continue; 38 | } 39 | 40 | yield groupObject; 41 | } 42 | }, 43 | 44 | uniqueElements: function*() { 45 | let observed = new Map(); 46 | 47 | for (let groupObject of _public.generators.trackingConfig()) { 48 | for (let element of groupObject.selectedElements) { 49 | if (observed.has(element)) { 50 | continue; 51 | } 52 | 53 | observed.set(element, true); 54 | yield element; 55 | } 56 | } 57 | }, 58 | 59 | }; 60 | 61 | _public.processElement = function(element, groupObject) { 62 | if (Config.DOMProperties.has(element)) { 63 | let DOMProperties = Config.DOMProperties.get(element); 64 | 65 | for (let event of groupObject.eventsList) { 66 | if (DOMProperties.hasEvent(event)) { 67 | let existingEventGroupName = DOMProperties.getEventGroupName(event); 68 | let existingSelector = Config.elementTrackingConfig.getElementGroup(existingEventGroupName).selector; 69 | 70 | // May not be necessary; good to have this sanity check in place, however. 71 | if (existingEventGroupName == groupObject.name) { 72 | continue; 73 | } 74 | 75 | let specificityComputation = (specificityCompare(existingSelector, groupObject.selector) == -1); 76 | 77 | if (Config.getConfigProperty('overrideEqualSpecificity')) { 78 | specificityComputation = (specificityCompare(existingSelector, groupObject.selector) <= 0); 79 | } 80 | 81 | if (specificityComputation) { 82 | DOMProperties.deleteEventsWithGroup(existingEventGroupName); 83 | DOMProperties.setEvent(event, groupObject.name); 84 | } 85 | } 86 | else { 87 | DOMProperties.setEvent(event, groupObject.name); 88 | //Config.DOMProperties.set(DOMProperties); 89 | } 90 | } 91 | } 92 | else { 93 | let DOMProperties = DOMPropertiesObject(groupObject); 94 | Config.DOMProperties.set(element, DOMProperties); 95 | } 96 | }; 97 | 98 | function getEventsList(event) { 99 | let eventsList = null; 100 | 101 | if (EventHandlerController.getEventHandler(event)) { 102 | eventsList = EventHandlerController.getEventHandlerEvents(event); 103 | 104 | if (!eventsList) { 105 | return undefined; 106 | } 107 | } 108 | else { 109 | eventsList = [event]; 110 | } 111 | 112 | return eventsList; 113 | }; 114 | 115 | return _public; 116 | })(window); -------------------------------------------------------------------------------- /src/modules/DOMHandler/mutationObserverController.js: -------------------------------------------------------------------------------- 1 | /* 2 | LogUI Client Library 3 | DOM Handler Helpers Module 4 | 5 | IIFE function that provides a controller for a global MutationObserver (checking for DOM changes). 6 | Calls the necessary events to bind event listeners to new elements (if they match a rule). 7 | 8 | @module: Mutation Observer Controller Handler 9 | @author: David Maxwell 10 | @date: 2020-03-02 11 | */ 12 | 13 | import Binder from './binder'; 14 | import DOMHandlerHelpers from './helpers'; 15 | 16 | export default (function(root) { 17 | var _public = {}; 18 | var _mutationObserver = null; 19 | 20 | _public.init = function() { 21 | _mutationObserver = new MutationObserver(observerCallback); 22 | 23 | let options = { 24 | childList: true, 25 | attributes: false, 26 | characterData: false, 27 | subtree: true, 28 | }; 29 | 30 | _mutationObserver.observe(root.document, options); 31 | }; 32 | 33 | _public.stop = function() { 34 | _mutationObserver.disconnect(); 35 | _mutationObserver = null; 36 | }; 37 | 38 | var observerCallback = function(mutationsList) { 39 | for (let record of mutationsList) { 40 | if (record.type == 'childList') { 41 | for (let element of record.addedNodes) { 42 | if (element.nodeType == 1) { 43 | processAddedElement(element); 44 | 45 | // There may be child elements that need to be processed, too. 46 | // The recurive function processDescendants handles this. 47 | processDescendants(element); 48 | } 49 | } 50 | } 51 | } 52 | }; 53 | 54 | var processDescendants = function(element) { 55 | let childArray = Array.from(element.children); 56 | 57 | childArray.forEach((childElement) => { 58 | processAddedElement(childElement); 59 | processDescendants(childElement); 60 | }); 61 | }; 62 | 63 | var processAddedElement = function(element) { 64 | let shallBind = false; 65 | 66 | for (let groupObject of DOMHandlerHelpers.generators.trackingConfig()) { 67 | if (element.matches(groupObject.selector)) { 68 | shallBind = true; 69 | DOMHandlerHelpers.processElement(element, groupObject); 70 | } 71 | } 72 | 73 | if (shallBind) { 74 | Binder.bind(element); 75 | } 76 | }; 77 | 78 | return _public; 79 | })(window); -------------------------------------------------------------------------------- /src/modules/browserEvents/contextMenu.js: -------------------------------------------------------------------------------- 1 | /* 2 | LogUI Client Library 3 | Browser Events / Context Menu Module 4 | 5 | A IIFE function yielding a module that provides functionality for tracking the appearance of the context menu. 6 | 7 | @module: Context Menu Tracking Module 8 | @author: David Maxwell 9 | @date: 2021-03-04 10 | */ 11 | 12 | import Config from '../config'; 13 | import EventPackager from './../eventPackager'; 14 | 15 | export default (function(root) { 16 | var _handler = {}; 17 | 18 | _handler.init = function() { 19 | if (Config.browserEventsConfig.get('contextMenu', true)) { 20 | root.document.addEventListener('contextmenu', callback); 21 | } 22 | }; 23 | 24 | _handler.stop = function() { 25 | root.document.removeEventListener('contextmenu', callback); 26 | }; 27 | 28 | var callback = function(event) { 29 | EventPackager.packageBrowserEvent({ 30 | type: 'contextMenuFired', 31 | }); 32 | }; 33 | 34 | return _handler; 35 | })(window); -------------------------------------------------------------------------------- /src/modules/browserEvents/cursorPosition.js: -------------------------------------------------------------------------------- 1 | /* 2 | LogUI Client Library 3 | Browser Events / Mouse Tracker Module 4 | 5 | A IIFE function yielding a module that provides functionality for tracking mouse movements. 6 | 7 | @module: URL Change Browser Event 8 | @author: David Maxwell 9 | @date: 2021-03-02 10 | */ 11 | 12 | import Config from '../config'; 13 | import EventPackager from './../eventPackager'; 14 | 15 | const CURSORPOSITION_TRACK_FREQUENCY = 200; 16 | 17 | export default (function(root) { 18 | var _handler = {}; 19 | var _trackLeaving = null; 20 | var _updateFrequency = CURSORPOSITION_TRACK_FREQUENCY; 21 | var _updateIntervalID = null; 22 | 23 | var _lastEvent = null; 24 | var _hadFocus = null; 25 | 26 | _handler.init = function() { 27 | if (Config.browserEventsConfig.get('trackCursor', true)) { 28 | let configUpdateFrequencyValue = Config.browserEventsConfig.get('cursorUpdateFrequency', CURSORPOSITION_TRACK_FREQUENCY); 29 | 30 | if (configUpdateFrequencyValue <= 0) { 31 | _updateFrequency = false; 32 | } 33 | else { 34 | _updateFrequency = configUpdateFrequencyValue; 35 | } 36 | 37 | _trackLeaving = Config.browserEventsConfig.get('cursorLeavingPage', true); 38 | 39 | root.document.addEventListener('mousemove', movementCallback); 40 | 41 | if (_trackLeaving) { 42 | root.document.addEventListener('mouseleave', pageLeaveCallback); 43 | root.document.addEventListener('mouseenter', pageEnterCallback); 44 | } 45 | 46 | intervalTimerSet(); 47 | } 48 | }; 49 | 50 | _handler.stop = function() { 51 | root.document.removeEventListener('mousemove', movementCallback); 52 | root.document.removeEventListener('mouseleave', pageLeaveCallback); 53 | root.document.removeEventListener('mouseenter', pageEnterCallback); 54 | 55 | intervalTimerClear(); 56 | _trackLeaving = null; 57 | _updateFrequency = CURSORPOSITION_TRACK_FREQUENCY; 58 | _lastEvent = null; 59 | _hadFocus = null; 60 | }; 61 | 62 | var movementCallback = function(event) { 63 | if (!_updateFrequency) { 64 | handleMousePosition(event, root.document.hasFocus()); 65 | } 66 | 67 | _lastEvent = event; 68 | _hadFocus = root.document.hasFocus(); 69 | }; 70 | 71 | var intervalTimerCallback = function() { 72 | if (!_lastEvent) { 73 | return; 74 | } 75 | 76 | handleMousePosition(_lastEvent, _hadFocus); 77 | }; 78 | 79 | var intervalTimerSet = function() { 80 | if (_updateFrequency && !_updateIntervalID) { 81 | _updateIntervalID = setInterval(intervalTimerCallback, _updateFrequency); 82 | } 83 | }; 84 | 85 | var intervalTimerClear = function() { 86 | clearInterval(_updateIntervalID); 87 | _updateIntervalID = null; 88 | }; 89 | 90 | var getBasicTrackingObject = function(event, hasFocus) { 91 | return { 92 | clientX: event.clientX, 93 | clientY: event.clientY, 94 | screenX: event.screenX, 95 | screenY: event.screenY, 96 | pageX: event.pageX, 97 | pageY: event.pageY, 98 | pageHadFocus: hasFocus, 99 | }; 100 | } 101 | 102 | var handleMousePosition = function(event, hasFocus) { 103 | let returnObject = getBasicTrackingObject(event, hasFocus); 104 | returnObject.type = 'cursorTracking'; 105 | returnObject.trackingType = 'positionUpdate'; 106 | 107 | EventPackager.packageBrowserEvent(returnObject); 108 | } 109 | 110 | var pageLeaveCallback = function(event) { 111 | let returnObject = getBasicTrackingObject(event, _hadFocus); 112 | returnObject.type = 'cursorTracking'; 113 | returnObject.trackingType = 'cursorLeftViewport'; 114 | 115 | intervalTimerClear(); 116 | EventPackager.packageBrowserEvent(returnObject); 117 | } 118 | 119 | var pageEnterCallback = function(event) { 120 | let returnObject = getBasicTrackingObject(event, _hadFocus); 121 | returnObject.type = 'cursorTracking'; 122 | returnObject.trackingType = 'cursorEnteredViewport'; 123 | 124 | intervalTimerSet(); 125 | EventPackager.packageBrowserEvent(returnObject); 126 | }; 127 | 128 | return _handler; 129 | })(window); -------------------------------------------------------------------------------- /src/modules/browserEvents/pageFocus.js: -------------------------------------------------------------------------------- 1 | /* 2 | LogUI Client Library 3 | Browser Events / Page Focus Event 4 | 5 | A IIFE function yielding a module that listens for when the page focus is lost or gained. 6 | 7 | @module: URL Change Browser Event 8 | @author: David Maxwell 9 | @date: 2021-03-02 10 | */ 11 | 12 | import Config from '../config'; 13 | import EventPackager from './../eventPackager'; 14 | 15 | export default (function(root) { 16 | var _handler = {}; 17 | 18 | _handler.init = function() { 19 | if (Config.browserEventsConfig.get('pageFocus', true)) { 20 | root.addEventListener('blur', callback); 21 | root.addEventListener('focus', callback); 22 | } 23 | }; 24 | 25 | _handler.stop = function() { 26 | root.removeEventListener('blur', callback); 27 | root.removeEventListener('focus', callback); 28 | }; 29 | 30 | var callback = function(event) { 31 | let pageHasFocus = (event.type === 'focus'); 32 | 33 | EventPackager.packageBrowserEvent({ 34 | type: 'viewportFocusChange', 35 | hasFocus: pageHasFocus, 36 | }); 37 | }; 38 | 39 | return _handler; 40 | })(window); -------------------------------------------------------------------------------- /src/modules/browserEvents/scroll.js: -------------------------------------------------------------------------------- 1 | /* 2 | LogUI Client Library 3 | Browser Events / Scroll Event 4 | 5 | A IIFE function yielding a module that listens for page scrolls. 6 | Also provides functionality to pause listeners when scrolling is taking place. 7 | 8 | @module: Scroll Browser Event 9 | @author: David Maxwell 10 | @date: 2021-03-02 11 | */ 12 | 13 | // import RequiredFeatures from '../required'; 14 | 15 | // RequiredFeatures.addFeature('IntersectionObserver'); 16 | 17 | import Config from '../config'; 18 | 19 | export default (function(root) { 20 | var _handler = {}; 21 | var _isScrolling = false; 22 | 23 | _handler.init = function() { 24 | if (Config.browserEventsConfig.get('eventsWhileScrolling', true)) { 25 | Config.CSSRules.addRule('.disable-hover, disable-hover *', 'pointer-events: none !important;'); 26 | root.addEventListener('scroll', callback); 27 | } 28 | }; 29 | 30 | _handler.stop = function() { 31 | root.removeEventListener('scroll', callback); 32 | } 33 | 34 | var callback = function(event) { 35 | // Setting the timeout to zero should mean the timeout fires when the callback has completed. 36 | // See https://stackoverflow.com/a/25614886 37 | _isScrolling = setTimeout(() => { 38 | root.document.body.classList.remove('disable-hover'); 39 | }, 0); 40 | 41 | if (!root.document.body.classList.contains('disable-hover')) { 42 | root.document.body.classList.add('disable-hover'); 43 | } 44 | }; 45 | 46 | return _handler; 47 | })(window); -------------------------------------------------------------------------------- /src/modules/browserEvents/urlChange.js: -------------------------------------------------------------------------------- 1 | /* 2 | LogUI Client Library 3 | Browser Events / URL Change Event 4 | 5 | A IIFE function yielding a module that listens for changes to the URL. 6 | 7 | @module: URL Change Browser Event 8 | @author: David Maxwell 9 | @date: 2021-03-02 10 | */ 11 | 12 | import Config from '../config'; 13 | import EventPackager from './../eventPackager'; 14 | 15 | export default (function(root) { 16 | var _handler = {}; 17 | var _existingPath = root.location.href; 18 | 19 | _handler.init = function() { 20 | if (Config.browserEventsConfig.get('URLChanges', true)) { 21 | root.addEventListener('popstate', callback); 22 | } 23 | }; 24 | 25 | _handler.stop = function() { 26 | root.removeEventListener('popstate', callback); 27 | }; 28 | 29 | var callback = function(event) { 30 | let previousPath = _existingPath; 31 | let currentPath = root.location.href; 32 | _existingPath = currentPath; 33 | 34 | EventPackager.packageBrowserEvent({ 35 | type: 'URLChange', 36 | previousURL: previousPath, 37 | newURL: currentPath, 38 | }); 39 | }; 40 | 41 | return _handler; 42 | })(window); -------------------------------------------------------------------------------- /src/modules/browserEvents/viewportResize.js: -------------------------------------------------------------------------------- 1 | /* 2 | LogUI Client Library 3 | Browser Events / Page Resize Event 4 | 5 | A IIFE function yielding a module that listens for page resize events. 6 | Adds some intelligent code that ensures the start and end resize events are logged -- not all of the ones in the middle. 7 | 8 | @module: Page Resize Event Module 9 | @author: David Maxwell 10 | @date: 2021-03-04 11 | */ 12 | 13 | import Config from '../config'; 14 | import EventPackager from '../eventPackager'; 15 | 16 | export default (function(root) { 17 | var _handler = {}; 18 | var _timeoutID = null; 19 | 20 | _handler.init = function() { 21 | if (Config.browserEventsConfig.get('pageResize', true)) { 22 | root.addEventListener('resize', callback); 23 | } 24 | }; 25 | 26 | _handler.stop = function() { 27 | root.removeEventListener('resize', callback); 28 | 29 | clearTimeout(_timeoutID); 30 | _timeoutID = null; 31 | } 32 | 33 | var callback = function(event) { 34 | clearTimeout(_timeoutID); 35 | 36 | _timeoutID = setTimeout(() => { 37 | EventPackager.packageBrowserEvent({ 38 | type: 'viewportResize', 39 | viewportWidth: event.target.innerWidth, 40 | viewportHeight: event.target.innerHeight, 41 | stringRepr: `${event.target.innerWidth}x${event.target.innerHeight}`, 42 | }); 43 | }, 200); 44 | }; 45 | 46 | return _handler; 47 | })(window); -------------------------------------------------------------------------------- /src/modules/config.js: -------------------------------------------------------------------------------- 1 | /* 2 | LogUI Client Library 3 | Configuration Module 4 | 5 | An IIFE function returning the configuration module for LogUI. 6 | Hosts configuration options and a variety of methods that are related to the configuration of the library. 7 | 8 | @module: Helpers 9 | @author: David Maxwell 10 | @date: 2020-10-07 11 | */ 12 | 13 | import Helpers from './helpers'; 14 | import Defaults from './defaults'; 15 | import RequiredFeatures from './required'; 16 | import ValidationSchemas from './validationSchemas'; 17 | 18 | const LOGUI_SESSION_ID_KEYNAME = 'logUI-sessionIDKey'; 19 | 20 | export default (function(root) { 21 | var _public = {}; 22 | var _initTimestamp = null; 23 | 24 | var _sessionData = null; 25 | 26 | var _applicationSpecificData = {}; 27 | var _trackingConfig = {}; 28 | var _configProperties = null; 29 | 30 | var _browserEvents = {}; 31 | 32 | var _DOMProperties = null; // Used to store properties for DOM elements on the page. 33 | 34 | var _styleElement = null; 35 | 36 | _public.init = function(suppliedConfigObject) { 37 | _initTimestamp = new Date(); 38 | _configProperties = Helpers.extendObject({}, Defaults); 39 | 40 | let initState = ( 41 | isSupported() && 42 | validateSuppliedConfigObject(suppliedConfigObject) && 43 | initConfigObjects(suppliedConfigObject) 44 | ); 45 | 46 | if (!initState) { 47 | _initTimestamp = null; 48 | return initState; 49 | } 50 | 51 | _DOMProperties = new WeakMap(); 52 | _public.CSSRules.init(); 53 | _public.sessionData.init(); 54 | return initState; 55 | }; 56 | 57 | var initConfigObjects = function(suppliedConfigObject) { 58 | Helpers.extendObject(_configProperties, Defaults.dispatcher); // Apply the defaults for the dispatcher. 59 | Helpers.extendObject(_configProperties, suppliedConfigObject.logUIConfiguration); // Apply the logUIConfiguration values from the supplied config object. 60 | 61 | _applicationSpecificData = suppliedConfigObject.applicationSpecificData; 62 | _trackingConfig = suppliedConfigObject.trackingConfiguration; 63 | _browserEvents = suppliedConfigObject.browserEvents; 64 | 65 | return true; 66 | }; 67 | 68 | _public.reset = function() { 69 | _configProperties = null; 70 | _initTimestamp = null; 71 | _sessionData = null; 72 | 73 | _applicationSpecificData = {}; 74 | _trackingConfig = {}; 75 | _browserEvents = {}; 76 | 77 | _public.CSSRules.reset(); 78 | }; 79 | 80 | _public.DOMProperties = { 81 | has: function(element) { 82 | return _DOMProperties.has(element); 83 | }, 84 | 85 | set: function(element, properties) { 86 | _DOMProperties.set(element, properties); 87 | }, 88 | 89 | get: function(element) { 90 | if (_DOMProperties.has(element)) { 91 | return _DOMProperties.get(element); 92 | } 93 | 94 | return undefined; 95 | }, 96 | 97 | reset: function() { 98 | if (_public.isActive()) { 99 | _DOMProperties = new WeakMap(); 100 | return true; 101 | } 102 | 103 | return false; 104 | } 105 | }; 106 | 107 | _public.CSSRules = { 108 | init: function() { 109 | _styleElement = root.document.createElement('style'); 110 | root.document.head.append(_styleElement); 111 | }, 112 | 113 | reset: function() { 114 | _styleElement.remove(); 115 | _styleElement = null; 116 | }, 117 | 118 | addRule: function(selectorString, propertiesString) { 119 | let stylesheet = _styleElement.sheet; 120 | 121 | if (stylesheet) { 122 | stylesheet.insertRule(`${selectorString} \{ ${propertiesString} \}`); 123 | } 124 | }, 125 | 126 | removeRule: function(selectorString, propertiesString) { 127 | if (_styleElement) { 128 | for (let i in _styleElement.sheet.cssRules) { 129 | let styleElement = _styleElement.sheet.cssRules[i]; 130 | 131 | if (styleElement.cssText == `${selectorString} \{ ${propertiesString} \}`) { 132 | _styleElement.sheet.removeRule(i); 133 | return true; 134 | } 135 | } 136 | 137 | return false; 138 | } 139 | }, 140 | }; 141 | 142 | _public.getConfigProperty = function(propertyName) { 143 | return _configProperties[propertyName]; 144 | }; 145 | 146 | // _public.getApplicationSpecificData = function() { 147 | // return _applicationSpecificData; 148 | // }; 149 | 150 | _public.applicationSpecificData = { 151 | get: function() { 152 | return _applicationSpecificData; 153 | }, 154 | 155 | update: function(updatedObject) { 156 | _applicationSpecificData = Helpers.extendObject(_applicationSpecificData, updatedObject); 157 | }, 158 | 159 | deleteKey: function(keyName) { 160 | delete _applicationSpecificData[keyName]; 161 | }, 162 | }; 163 | 164 | _public.elementTrackingConfig = { 165 | get: function() { 166 | return _trackingConfig; 167 | }, 168 | 169 | getElementGroup: function(groupName) { 170 | return _trackingConfig[groupName]; 171 | }, 172 | }; 173 | 174 | _public.isActive = function() { 175 | return (!!_initTimestamp); 176 | }; 177 | 178 | _public.getInitTimestamp = function() { 179 | return _initTimestamp; 180 | } 181 | 182 | _public.sessionData = { 183 | init: function() { 184 | _sessionData = { 185 | IDkey: null, 186 | sessionStartTimestamp: null, 187 | libraryStartTimestamp: null, 188 | }; 189 | 190 | _public.sessionData.getSessionIDKey(); 191 | }, 192 | 193 | reset: function() { 194 | _public.sessionData.init(); 195 | }, 196 | 197 | getSessionIDKey: function() { 198 | return root.sessionStorage.getItem(LOGUI_SESSION_ID_KEYNAME); 199 | }, 200 | 201 | clearSessionIDKey: function() { 202 | root.sessionStorage.removeItem(LOGUI_SESSION_ID_KEYNAME); 203 | }, 204 | 205 | setID: function(newID) { 206 | _sessionData.IDkey = newID; 207 | root.sessionStorage.setItem(LOGUI_SESSION_ID_KEYNAME, newID); 208 | }, 209 | 210 | setIDFromSession: function() { 211 | _sessionData.IDKey = root.sessionStorage.getItem(LOGUI_SESSION_ID_KEYNAME); 212 | }, 213 | 214 | setTimestamps: function(sessionStartTimestamp, libraryLoadTimestamp) { 215 | _sessionData.sessionStartTimestamp = sessionStartTimestamp; 216 | _sessionData.libraryStartTimestamp = libraryLoadTimestamp; 217 | }, 218 | 219 | getSessionStartTimestamp: function() { 220 | return _sessionData.sessionStartTimestamp; 221 | }, 222 | 223 | getLibraryStartTimestamp: function() { 224 | return _sessionData.libraryStartTimestamp; 225 | }, 226 | } 227 | 228 | _public.browserEventsConfig = { 229 | get: function(propertyName, defaultValue) { 230 | if (_public.browserEventsConfig.has(propertyName)) { 231 | return _browserEvents[propertyName]; 232 | } 233 | 234 | return defaultValue; 235 | }, 236 | 237 | has: function(propertyName) { 238 | if (_browserEvents) { 239 | return _browserEvents.hasOwnProperty(propertyName); 240 | } 241 | 242 | return false; 243 | }, 244 | } 245 | 246 | var isSupported = function() { 247 | for (const feature of RequiredFeatures.getFeatures()) { 248 | if (!Helpers.getElementDescendant(root, feature)) { 249 | Helpers.console(`The required feature '${feature}' cannot be found; LogUI cannot start!`, 'Initialisation', true); 250 | return false; 251 | } 252 | } 253 | 254 | return true; 255 | }; 256 | 257 | var validateSuppliedConfigObject = function(suppliedConfigObject) { 258 | let validator = ValidationSchemas.validateSuppliedConfigObject(suppliedConfigObject); 259 | 260 | if (!validator.valid) { 261 | Helpers.console(`The configuration object passed to LogUI was not valid or complete; refer to the warning(s) below for more information.`, 'Initialisation', true); 262 | 263 | for (let error of validator.errors) { 264 | Helpers.console(`> ${error.stack}`, 'Initialisation', true); 265 | } 266 | 267 | return false; 268 | } 269 | 270 | return true; 271 | }; 272 | 273 | return _public; 274 | })(window); -------------------------------------------------------------------------------- /src/modules/defaults.js: -------------------------------------------------------------------------------- 1 | const logUIdefaults = { 2 | verbose: true, // Whether LogUI dumps events to console.log() or not. 3 | overrideEqualSpecificity: true, // If an existing event has equal specificity to the event being proposed, do we replace it (true, default) or replace it (false)? 4 | sessionUUID: null, // The session UUID to be used (null means no previous UUID has been used). 5 | }; 6 | 7 | export default logUIdefaults; -------------------------------------------------------------------------------- /src/modules/dispatchers/consoleDispatcher.js: -------------------------------------------------------------------------------- 1 | /* 2 | LogUI Client Library 3 | Page-based Dispatcher 4 | 5 | A dispatcher object that outputs elements to a "console" element on a page. 6 | No network or server activity involved. 7 | 8 | @module: Page-based Dispatcher 9 | @author: David Maxwell 10 | @date: 2020-09-20 11 | */ 12 | 13 | import Config from '../config'; 14 | import Helpers from '../helpers' 15 | import Defaults from '../defaults'; 16 | import RequiredFeatures from '../required'; 17 | import ValidationSchemas from '../validationSchemas'; 18 | 19 | Defaults.dispatcher = { 20 | consoleElement: null, // The element that the console is rendered in. 21 | } 22 | 23 | RequiredFeatures.addFeature('document.createElement'); 24 | RequiredFeatures.addFeature('document.createTextNode'); 25 | 26 | ValidationSchemas.addLogUIConfigProperty('consoleElement', 'string'); 27 | 28 | export default (function(root) { 29 | var _public = {}; 30 | var _isActive = false; 31 | var _consoleElement = null; 32 | _public.dispatcherType = 'console'; 33 | 34 | _public.init = async function() { 35 | return new Promise(function(resolve) { 36 | let initState = ( 37 | doesConsoleElementExist() && 38 | getSessionDetails() 39 | ); 40 | 41 | if (initState) { 42 | _isActive = true; 43 | }; 44 | 45 | root.dispatchEvent(new Event('logUIStarted')); 46 | resolve(initState); 47 | }); 48 | }; 49 | 50 | _public.isActive = function() { 51 | return _isActive; 52 | } 53 | 54 | _public.sendObject = function(objToSend) { 55 | createElement(objToSend); 56 | }; 57 | 58 | _public.stop = function() { 59 | return new Promise(resolve => { 60 | setTimeout(() => {_isActive = false; resolve(true)}, 1000); 61 | }); 62 | }; 63 | 64 | function doesConsoleElementExist() { 65 | let consoleElementString = Config.getConfigProperty('consoleElement'); 66 | _consoleElement = Helpers.$(consoleElementString); 67 | 68 | if (!_consoleElement) { 69 | Helpers.console(`The dispatcher cannot find the specified console element (${consoleElementString}) in the DOM.`, 'Initialisation', true); 70 | return false; 71 | } 72 | 73 | return true; 74 | }; 75 | 76 | function createElement(objToSend) { 77 | let newNode = document.createElement('li'); 78 | 79 | newNode.appendChild(document.createTextNode(objToSend.eventType)); 80 | newNode.appendChild(document.createElement('br')); 81 | newNode.appendChild(document.createTextNode(getEventDetails(objToSend))); 82 | _consoleElement.insertBefore(newNode, _consoleElement.firstChild); 83 | 84 | return newNode; 85 | }; 86 | 87 | function getEventDetails(objToSend) { 88 | switch (objToSend.eventType) { 89 | case 'interactionEvent': 90 | return objToSend.eventDetails.type; 91 | case 'browserEvent': 92 | switch (objToSend.eventDetails.type) { 93 | case 'contextMenuFired': 94 | return 'Context menu requested'; 95 | case 'cursorTracking': 96 | switch (objToSend.eventDetails.trackingType) { 97 | case 'positionUpdate': 98 | return `Cursor at ${objToSend.eventDetails.clientX},${objToSend.eventDetails.clientY} in viewport`; 99 | case 'cursorLeftViewport': 100 | return `Cursor left viewport at ${objToSend.eventDetails.clientX},${objToSend.eventDetails.clientY}`; 101 | case 'cursorEnteredViewport': 102 | return `Cursor entered viewport at ${objToSend.eventDetails.clientX},${objToSend.eventDetails.clientY}`; 103 | }; 104 | case 'viewportFocusChange': 105 | if (objToSend.eventDetails.hasFocus) { 106 | return 'Viewport is now the active window'; 107 | } 108 | else { 109 | return 'Viewport focus has been lost'; 110 | } 111 | case 'URLChange': 112 | return `URL changed in browser to ${objToSend.eventDetails.newURL}`; 113 | case 'viewportResize': 114 | return `Viewport resized to ${objToSend.eventDetails.viewportWidth}x${objToSend.eventDetails.viewportHeight}`; 115 | } 116 | case 'statusEvent': 117 | switch (objToSend.eventDetails.type) { 118 | case 'started': 119 | return 'LogUI Started'; 120 | case 'stopped': 121 | return 'LogUI Stopped'; 122 | case 'applicationSpecificDataUpdated': 123 | return 'Application specific data was updated'; 124 | } 125 | } 126 | } 127 | 128 | function getSessionDetails() { 129 | let currentTimestamp = new Date(); 130 | 131 | if (Config.sessionData.getSessionIDKey()) { 132 | Config.sessionData.setIDFromSession(); 133 | Config.sessionData.setTimestamps(currentTimestamp, currentTimestamp); // The first date should come from the server (for the session start time). 134 | 135 | //return false; // If the server disagrees with the key supplied, you'd return false here to fail the initialisation. 136 | } 137 | else { 138 | // Create a new session. 139 | // For the websocket dispatcher, we'd send off a blank session ID field, and it will return a new one. 140 | Config.sessionData.setID('CONSOLE-SESSION-ID'); // ID should come from the server in the websocket dispatcher. 141 | Config.sessionData.setTimestamps(currentTimestamp, currentTimestamp); 142 | } 143 | 144 | return true; 145 | }; 146 | 147 | return _public; 148 | })(window); -------------------------------------------------------------------------------- /src/modules/dispatchers/websocketDispatcher.js: -------------------------------------------------------------------------------- 1 | /* 2 | LogUI Client Library 3 | WebSocket-based Dispatcher 4 | 5 | A WebSocket-based dispatcher that communicates with a LogUI server implementation. 6 | 7 | @module: WebSocket-based Dispatcher 8 | @author: David Maxwell 9 | @date: 2021-03-08 10 | */ 11 | 12 | import Config from '../config'; 13 | import Helpers from '../helpers'; 14 | import Defaults from '../defaults'; 15 | import RequiredFeatures from '../required'; 16 | import ValidationSchemas from '../validationSchemas'; 17 | 18 | Defaults.dispatcher = { 19 | endpoint: null, // The URL of the WebSocket endpoint to send data to. 20 | authorisationToken: null, // The string representing the authentication token to connect to the endpoint with. 21 | cacheSize: 10, // The maximum number of stored events that can be in the cache before flushing. 22 | maximumCacheSize: 1000, // When no connection is present, this is the cache size we shut down LogUI at. 23 | reconnectAttempts: 5, // The maximum number of times we should try to reconnect. 24 | reconnectAttemptDelay: 5000 // The delay (in ms) we should wait between reconnect attempts. 25 | }; 26 | 27 | RequiredFeatures.addFeature('WebSocket'); 28 | 29 | ValidationSchemas.addLogUIConfigProperty('endpoint', 'string'); 30 | ValidationSchemas.addLogUIConfigProperty('authorisationToken', 'string'); 31 | 32 | export default (function(root) { 33 | var _public = {}; 34 | var _isActive = false; 35 | var _websocket = null; 36 | var _websocketReconnectionAttempts = 0; // The total number of attempts that have been made to reconnect when the connection drops. 37 | var _websocketSuccessfulReconnections = 0; // The total number of times there has been a successful (re)connection. 38 | var _websocketReconnectionReference = null; // A reference to the reconnection routine when attempting to reconnect. 39 | var _libraryLoadTimestamp = null; // The time at which the dispatcher loads -- for measuring the beginning of a session more accurately. 40 | 41 | var _cache = null; 42 | 43 | _public.dispatcherType = 'websocket'; 44 | 45 | _public.init = function() { 46 | Config.getConfigProperty('endpoint'); 47 | 48 | // We may restart the dispatcher in the same context. 49 | // There may still be a timer active from the previous iteration. 50 | // If so, we cancel it. 51 | if (_websocketReconnectionReference) { 52 | clearInterval(_websocketReconnectionReference); 53 | _websocketReconnectionReference = null; 54 | } 55 | 56 | _initWebsocket(); 57 | 58 | _cache = []; 59 | _isActive = true; 60 | return true; 61 | }; 62 | 63 | _public.stop = async function() { 64 | _flushCache(); 65 | _tidyWebsocket(); 66 | 67 | _websocketReconnectionAttempts = 0; 68 | _websocketSuccessfulReconnections = 0; 69 | _libraryLoadTimestamp = null; 70 | 71 | if (_websocketReconnectionReference) { 72 | clearInterval(_websocketReconnectionReference); 73 | } 74 | 75 | _cache = null; 76 | _isActive = false; 77 | }; 78 | 79 | _public.isActive = function() { 80 | return _isActive; 81 | }; 82 | 83 | _public.sendObject = function(objectToSend) { 84 | if (_isActive) { 85 | _cache.push(objectToSend); 86 | Helpers.console(objectToSend, 'Dispatcher', false); 87 | 88 | if (_cache.length >= Defaults.dispatcher.cacheSize) { 89 | _flushCache(); 90 | } 91 | 92 | return; 93 | } 94 | 95 | throw Error('You cannot send a message when LogUI is not active.'); 96 | }; 97 | 98 | var _initWebsocket = function() { 99 | _websocket = new WebSocket(Config.getConfigProperty('endpoint')); 100 | 101 | _websocket.addEventListener('close', _callbacks.onClose); 102 | _websocket.addEventListener('error', _callbacks.onError); 103 | _websocket.addEventListener('message', _callbacks.onMessage); 104 | _websocket.addEventListener('open', _callbacks.onOpen); 105 | }; 106 | 107 | var _tidyWebsocket = function() { 108 | if (_websocket) { 109 | Helpers.console(`The connection to the server is being closed.`, 'Dispatcher', false); 110 | 111 | _websocket.removeEventListener('close', _callbacks.onClose); 112 | _websocket.removeEventListener('error', _callbacks.onError); 113 | _websocket.removeEventListener('message', _callbacks.onMessage); 114 | _websocket.removeEventListener('open', _callbacks.onOpen); 115 | 116 | _websocket.close(); 117 | _websocket = null; 118 | } 119 | }; 120 | 121 | var _attemptReconnect = function() { 122 | if (_websocket && !_websocketReconnectionReference) { 123 | _tidyWebsocket(); 124 | 125 | _websocketReconnectionReference = setInterval(() => { 126 | if (_isActive) { 127 | if (_websocket) { 128 | switch (_websocket.readyState) { 129 | case 0: 130 | return; 131 | case 1: 132 | Helpers.console(`The connection to the server has been (re-)established.`, 'Dispatcher', false); 133 | 134 | clearInterval(_websocketReconnectionReference); 135 | _websocketReconnectionAttempts = 0; 136 | _websocketReconnectionReference = null; 137 | 138 | return; 139 | default: 140 | Helpers.console(`The connection to the server has failed; we are unable to restart.`, 'Dispatcher', true); 141 | _tidyWebsocket(); 142 | return; 143 | } 144 | } 145 | 146 | // Counter incremented here to consider the first attempt. 147 | _websocketReconnectionAttempts += 1; 148 | 149 | if (_websocketReconnectionAttempts == Defaults.dispatcher.reconnectAttempts) { 150 | Helpers.console(`We've maxed out the number of permissible reconnection attempts. We must stop here.`, 'Dispatcher', true); 151 | 152 | clearInterval(_websocketReconnectionReference); 153 | root.dispatchEvent(new Event('logUIShutdownRequest')); 154 | throw Error('LogUI attempted to reconnect to the server but failed to do so. LogUI is now stopping. Any events not sent to the server will be lost.'); 155 | 156 | } 157 | 158 | Helpers.console(`(Re-)connection attempt ${_websocketReconnectionAttempts} of ${Defaults.dispatcher.reconnectAttempts}`, 'Dispatcher', false); 159 | _initWebsocket(); 160 | } 161 | else { 162 | // Here, the instance of LogUI has already been stopped. 163 | // So just silently clear the timer -- and reset the referene back to null. 164 | clearInterval(_websocketReconnectionReference); 165 | _websocketReconnectionReference = null; 166 | 167 | } 168 | }, Defaults.dispatcher.reconnectAttemptDelay); 169 | } 170 | 171 | }; 172 | 173 | var _callbacks = { 174 | onClose: function(event) { 175 | Helpers.console(`The connection to the server has been closed.`, 'Dispatcher', false); 176 | 177 | let errorMessage = 'Something went wrong with the connection to the LogUI server.' 178 | 179 | switch (event.code) { 180 | case 4001: 181 | errorMessage = 'A bad message was sent to the LogUI server. LogUI is shutting down.'; 182 | break; 183 | case 4002: 184 | errorMessage = 'The client sent a bad application handshake to the server. LogUI is shutting down.'; 185 | break; 186 | case 4003: 187 | errorMessage = 'The LogUI server being connected to does not support version __buildVersion__ of the client. LogUI is shutting down.'; 188 | break; 189 | case 4004: 190 | errorMessage = 'A bad authentication token was provided to the LogUI server. LogUI is shutting down.'; 191 | break; 192 | case 4005: 193 | errorMessage = 'The LogUI server did not recognise the domain that this client is being started from. LogUI is shutting down.'; 194 | break; 195 | case 4006: 196 | errorMessage = 'The LogUI client sent an invalid session ID to the server. LogUI is shutting down.'; 197 | Config.sessionData.clearSessionIDKey(); 198 | break; 199 | case 4007: 200 | errorMessage = 'The LogUI server is not accepting new connections for this application at present.'; 201 | break; 202 | default: 203 | errorMessage = `${errorMessage} The recorded error code was ${event.code}. LogUI is shutting down.`; 204 | break; 205 | } 206 | 207 | switch (event.code) { 208 | case 1000: 209 | console.log('clean connection closure!'); 210 | break; 211 | case 1006: 212 | _attemptReconnect(); 213 | break; 214 | default: 215 | root.dispatchEvent(new Event('logUIShutdownRequest')); 216 | throw Error(errorMessage); 217 | } 218 | }, 219 | 220 | onError: function(event) { }, 221 | 222 | onMessage: function(receivedMessage) { 223 | let messageObject = JSON.parse(receivedMessage.data); 224 | 225 | switch (messageObject.type) { 226 | case 'handshakeSuccess': 227 | Helpers.console(`The handshake was successful. Hurray! The server is listening.`, 'Dispatcher', false); 228 | Config.sessionData.setID(messageObject.payload.sessionID); 229 | 230 | if (messageObject.payload.newSessionCreated) { 231 | Config.sessionData.setTimestamps(new Date(messageObject.payload.clientStartTimestamp), new Date(messageObject.payload.clientStartTimestamp)); 232 | } 233 | else { 234 | Config.sessionData.setTimestamps(new Date(messageObject.payload.clientStartTimestamp), new Date()); 235 | 236 | if (_cache.length >= Defaults.dispatcher.cacheSize) { 237 | _flushCache(); 238 | } 239 | } 240 | 241 | // ADD CALL HERE 242 | root.dispatchEvent(new Event('logUIStarted')); 243 | break; 244 | } 245 | }, 246 | 247 | onOpen: function(event) { 248 | _websocketSuccessfulReconnections += 1; 249 | let sessionID = Config.sessionData.getSessionIDKey(); 250 | 251 | Helpers.console(`The connection to the server has been established.`, 'Dispatcher', false); 252 | 253 | let payload = { 254 | clientVersion: '__buildVersion__', 255 | authorisationToken: Config.getConfigProperty('authorisationToken'), 256 | pageOrigin: root.location.origin, 257 | userAgent: root.navigator.userAgent, 258 | clientTimestamp: new Date(), 259 | }; 260 | 261 | if (sessionID) { 262 | payload.sessionID = Config.sessionData.getSessionIDKey(); 263 | } 264 | 265 | Helpers.console(`The LogUI handshake has been sent.`, 'Dispatcher', false); 266 | _websocket.send(JSON.stringify(_getMessageObject('handshake', payload))); 267 | }, 268 | 269 | }; 270 | 271 | var _getMessageObject = function(messageType, payload) { 272 | return { 273 | sender: 'logUIClient', 274 | type: messageType, 275 | payload: payload, 276 | }; 277 | }; 278 | 279 | var _flushCache = function() { 280 | if (!_websocket || _websocket.readyState != 1) { 281 | if (_cache.length >= Defaults.dispatcher.maximumCacheSize) { 282 | Helpers.console(`The cache has grown too large, with no connection to clear it. LogUI will now stop; any cached events will be lost.`, 'Dispatcher', false); 283 | root.dispatchEvent(new Event('logUIShutdownRequest')); 284 | } 285 | 286 | return; 287 | } 288 | 289 | let payload = { 290 | length: _cache.length, 291 | items: _cache, 292 | }; 293 | 294 | _websocket.send(JSON.stringify(_getMessageObject('logEvents', payload))); 295 | Helpers.console(`Cache flushed.`, 'Dispatcher', false); 296 | 297 | _cache = []; 298 | }; 299 | 300 | return _public; 301 | })(window); -------------------------------------------------------------------------------- /src/modules/eventCallbackHandler.js: -------------------------------------------------------------------------------- 1 | /* 2 | LogUI Client Library 3 | Event Callback Handler Module 4 | 5 | An IIFE function returning functions for handling event callbacks associated with elements/events tracked under LogUI. 6 | 7 | @module: Event Callback Handler Module 8 | @author: David Maxwell 9 | @date: 2020-02-25 10 | */ 11 | 12 | import Config from './config'; 13 | import EventPackager from './eventPackager'; 14 | import EventHandlerController from './eventHandlerController'; 15 | 16 | export default (function(root) { 17 | var _public = {}; 18 | 19 | _public.logUIEventCallback = function(browserEvent) { 20 | let elementDOMProperties = Config.DOMProperties.get(browserEvent.currentTarget); 21 | 22 | // console.log("Event happened"); 23 | // console.log(browserEvent.target); 24 | // console.log(browserEvent.currentTarget); // This may be the correct thing to use instead of .target - need to test this some more. 25 | // console.log(browserEvent.eventPhase); 26 | // console.log(elementDOMProperties); 27 | // console.log('====='); 28 | 29 | // This stops event propogation, preventing multiple events being fired. 30 | // After testing, this doesn't seem to break hovering over children where a listener is present... 31 | // browserEvent.stopPropagation(); 32 | 33 | // stopPropogation() unfortunately also stops other bound event listeners not related to LogUI from firing. 34 | // Instead, we can check the eventPhase property of the event -- if we're at the target element (2), we can proceed. 35 | // If we are not at the target event (!=2) we do not proceed further with the logging process. 36 | // This should no longer be required (as of 2022-02-02) as we use currentTarget instead, alongside the check below to ensure that the object considered is covered by LogUI. 37 | // if (browserEvent.eventPhase != 2) { 38 | // return; 39 | // } 40 | 41 | // Can we work out what the call is for, and check? 42 | // like if we have a click on green, and a click on body, the element itself takes precedence? 43 | // So if there are multiple ones, can we use CSS specificty to work out what one to take forward? 44 | 45 | if (!elementDOMProperties) { 46 | return; // In this scenario, there is no matching DOMProperties object for the element. 47 | } 48 | 49 | let groupName = elementDOMProperties.getEventGroupName(browserEvent.type); 50 | let trackingConfig = Config.elementTrackingConfig.getElementGroup(groupName); 51 | let eventHandler = EventHandlerController.getEventHandler(trackingConfig.event); 52 | let packageEvent = false; 53 | 54 | if (eventHandler) { 55 | packageEvent = eventHandler.logUIEventCallback(this, browserEvent, trackingConfig); 56 | } 57 | else { 58 | packageEvent = _defaultEventCallbackHandler(this, browserEvent, trackingConfig); 59 | } 60 | 61 | if (packageEvent) { 62 | EventPackager.packageInteractionEvent(this, packageEvent, trackingConfig); 63 | } 64 | }; 65 | 66 | var _defaultEventCallbackHandler = function(eventContext, browserEvent, trackingConfig) { 67 | let returnObject = { 68 | type: browserEvent.type, 69 | }; 70 | 71 | if (trackingConfig.hasOwnProperty('name')) { 72 | returnObject.name = trackingConfig.name; 73 | } 74 | 75 | return returnObject; 76 | }; 77 | 78 | return _public; 79 | })(window); -------------------------------------------------------------------------------- /src/modules/eventHandlerController.js: -------------------------------------------------------------------------------- 1 | /* 2 | LogUI Client Library 3 | Event Handlers Controller Module 4 | 5 | A IIFE function yielding the event handler controller. 6 | Provides access to custom event handlers and built-in basic handlers. 7 | 8 | @module: Event Handler Controller 9 | @author: David Maxwell 10 | @date: 2020-09-21 11 | */ 12 | 13 | import Helpers from './helpers'; 14 | import ImportedEventHandlers from './eventHandlers/*'; 15 | 16 | export default (function(root) { 17 | var _public = {}; 18 | 19 | _public.init = function() { 20 | for (let eventHandler in ImportedEventHandlers) { 21 | if (ImportedEventHandlers[eventHandler].hasOwnProperty('init')) { 22 | ImportedEventHandlers[eventHandler].init(); 23 | } 24 | } 25 | 26 | return true; 27 | }; 28 | 29 | _public.stop = function() { 30 | for (let eventHandler in ImportedEventHandlers) { 31 | if (ImportedEventHandlers[eventHandler].hasOwnProperty('stop')) { 32 | ImportedEventHandlers[eventHandler].stop(); 33 | } 34 | } 35 | } 36 | 37 | _public.eventHandlers = ImportedEventHandlers; 38 | 39 | _public.getEventHandler = function(eventName) { 40 | if (ImportedEventHandlers.hasOwnProperty(eventName)) { 41 | return ImportedEventHandlers[eventName]; 42 | } 43 | 44 | return false; 45 | }; 46 | 47 | _public.getEventHandlerEvents = function(eventName) { 48 | if (ImportedEventHandlers.hasOwnProperty(eventName)) { 49 | if (ImportedEventHandlers[eventName].hasOwnProperty('browserEvents')) { 50 | return ImportedEventHandlers[eventName]['browserEvents']; 51 | } 52 | else { 53 | Helpers.console(`The event handler '${eventName}' does not have the required property 'browserEvents'.`, 'Initialisation', true); 54 | return false; 55 | } 56 | } 57 | 58 | return undefined; 59 | }; 60 | 61 | return _public; 62 | })(window); -------------------------------------------------------------------------------- /src/modules/eventHandlers/formSubmission.js: -------------------------------------------------------------------------------- 1 | /* 2 | LogUI Client Library 3 | Event Handlers / Form Submission Group Event 4 | 5 | A IIFE function yielding a form submission event. 6 | 7 | @module: Form Submission Event Handler 8 | @author: David Maxwell 9 | @date: 2021-03-25 10 | */ 11 | 12 | import Config from '../config'; 13 | import Helpers from '../helpers'; 14 | import MetadataHandler from '../metadataHandler'; 15 | 16 | export default (function(root) { 17 | var _handler = {}; 18 | 19 | _handler.browserEvents = ['submit']; 20 | 21 | _handler.init = function() { 22 | return; 23 | }; 24 | 25 | _handler.logUIEventCallback = function(eventContext, browserEvent, trackingConfig) { 26 | let customName = trackingConfig.name; 27 | let formElementValues = getFormElementValue(trackingConfig); 28 | let returnObject = { 29 | type: browserEvent.type, 30 | }; 31 | 32 | console.log(customName); 33 | 34 | if (customName) { 35 | returnObject.name = customName; 36 | } 37 | 38 | if (formElementValues.length > 0) { 39 | returnObject.submissionValues = formElementValues; 40 | } 41 | 42 | return returnObject; 43 | }; 44 | 45 | var getFormElementValue = function(trackingConfig) { 46 | let trackingConfigProperties = trackingConfig.properties; 47 | let returnArray = []; 48 | let observedNames = []; 49 | 50 | if (trackingConfigProperties && 51 | trackingConfigProperties.hasOwnProperty('includeValues')) { 52 | for (let entry of trackingConfigProperties.includeValues) { 53 | let element = Helpers.$(entry.selector); 54 | 55 | if (!element) { 56 | continue; 57 | } 58 | 59 | let returnedObject = MetadataHandler.getMetadataValue(element, entry); 60 | 61 | if (!returnedObject) { 62 | continue; 63 | } 64 | 65 | if (observedNames.includes(entry.nameForLog)) { 66 | continue; 67 | } 68 | 69 | observedNames.push(entry.nameForLog); 70 | returnArray.push(returnedObject); 71 | } 72 | } 73 | 74 | return returnArray; 75 | } 76 | 77 | return _handler; 78 | })(window); -------------------------------------------------------------------------------- /src/modules/eventHandlers/mouseClick.js: -------------------------------------------------------------------------------- 1 | /* 2 | LogUI Client Library 3 | Event Handlers / Mouse Click Group Event 4 | 5 | A IIFE function yielding a generic mouse click event. 6 | 7 | @module: Mouse Click Event Handler 8 | @author: David Maxwell 9 | @date: 2021-05-05 10 | */ 11 | 12 | import RequiredFeatures from '../required'; 13 | 14 | RequiredFeatures.addFeature('WeakMap'); 15 | 16 | export default (function(root) { 17 | var _handler = {}; 18 | var _currentMouseOver = null; 19 | 20 | _handler.browserEvents = ['mousedown', 'mouseup']; 21 | 22 | _handler.init = function() { 23 | _currentMouseOver = new WeakMap(); 24 | return; 25 | } 26 | 27 | _handler.stop = function() { 28 | _currentMouseOver = null; 29 | } 30 | 31 | _handler.logUIEventCallback = function(eventContext, browserEvent, trackingConfig) { 32 | if (browserEvent.type == 'mousedown') { 33 | _currentMouseOver.set(eventContext, browserEvent.timeStamp); 34 | } 35 | 36 | if (browserEvent.type == 'mouseup') { 37 | if (_currentMouseOver.has(eventContext)) { 38 | let difference = browserEvent.timeStamp - _currentMouseOver.get(eventContext); 39 | let button = browserEvent.button; 40 | let properties = getButtonConfig(trackingConfig, button); 41 | 42 | if (!properties) { 43 | // If no properties are specified, we ignore the event. 44 | return; 45 | } 46 | 47 | // If we get here, we should log this event. 48 | // Construct the returnObject, and send it back. This gets sent to the EventPackager. 49 | let returnObject = {}; 50 | returnObject['clickDuration'] = difference; 51 | returnObject['type'] = 'mouseClick'; 52 | returnObject['button'] = properties.mapping; 53 | 54 | if ('name' in properties) { 55 | returnObject['name'] = properties.name; 56 | } 57 | 58 | _currentMouseOver.delete(eventContext); 59 | return returnObject; 60 | } 61 | } 62 | 63 | }; 64 | 65 | var getButtonConfig = function(trackingConfig, button) { 66 | // Given a button number (0, 1, 2...), return the properties for that button. 67 | // Mappings: primary (0), auxiliary (1), secondary (2) 68 | if (!trackingConfig.properties) { 69 | // No properties object was found for the given configuration. 70 | return; 71 | } 72 | 73 | let mapping = { 74 | 0: 'primary', 75 | 1: 'auxiliary', 76 | 2: 'secondary', 77 | 3: 'auxiliary2', 78 | 4: 'auxiliary3', 79 | }; 80 | 81 | if (!(button in mapping)) { 82 | // The button ID clicked doesn't have a mapping in the object above. 83 | return; 84 | } 85 | 86 | if (!trackingConfig.properties[mapping[button]]) { 87 | // No configuration was found for the given mapping (e.g., no primary, auxiliary, secondary). 88 | return; 89 | } 90 | 91 | trackingConfig.properties[mapping[button]].mapping = mapping[button]; 92 | return trackingConfig.properties[mapping[button]]; 93 | } 94 | 95 | return _handler; 96 | })(window); 97 | 98 | -------------------------------------------------------------------------------- /src/modules/eventHandlers/mouseHover.js: -------------------------------------------------------------------------------- 1 | /* 2 | LogUI Client Library 3 | Event Handlers / Mouse Hover Group Event 4 | 5 | A IIFE function yielding a mouse hover event. 6 | 7 | @module: Mouse Hover Event Handler 8 | @author: David Maxwell 9 | @date: 2020-10-06 10 | */ 11 | 12 | import Config from '../config'; 13 | 14 | export default (function(root) { 15 | var _handler = {}; 16 | 17 | _handler.browserEvents = ['mouseenter', 'mouseleave']; 18 | 19 | _handler.init = function() { 20 | return; 21 | }; 22 | 23 | _handler.logUIEventCallback = function(eventContext, browserEvent, trackingConfig) { 24 | let customName = getEventName(trackingConfig, browserEvent.type); 25 | let returnObject = { 26 | type: browserEvent.type, 27 | }; 28 | 29 | if (customName) { 30 | returnObject.name = customName; 31 | } 32 | 33 | return returnObject; 34 | }; 35 | 36 | var getEventName = function(trackingConfig, eventName) { 37 | let trackingConfigProperties = trackingConfig.properties; 38 | 39 | if (trackingConfigProperties && 40 | trackingConfigProperties.hasOwnProperty(eventName) && 41 | trackingConfigProperties[eventName].hasOwnProperty('name')) { 42 | return trackingConfigProperties[eventName].name; 43 | } 44 | 45 | return undefined; 46 | } 47 | 48 | return _handler; 49 | })(window); -------------------------------------------------------------------------------- /src/modules/eventHandlers/sampleEventHandler.js: -------------------------------------------------------------------------------- 1 | /* 2 | LogUI Client Library 3 | Event Handlers / Sample Event Handler 4 | 5 | A IIFE function returning a sample event handler. 6 | Copy and paste this module to create your own handler. 7 | 8 | @module: Sample Event Handlers 9 | @author: David Maxwell 10 | @date: 2020-10-06 11 | */ 12 | 13 | import RequiredFeatures from '../required'; 14 | 15 | RequiredFeatures.addFeature('IntersectionObserver'); 16 | 17 | export default (function(root) { 18 | var _handler = {}; 19 | 20 | _handler.init = function() { 21 | return; 22 | }; 23 | 24 | _handler.callback = function(browserEvent, trackingConfig) { 25 | return true; 26 | } 27 | 28 | return _handler; 29 | })(window); -------------------------------------------------------------------------------- /src/modules/eventHandlers/scrollable.js: -------------------------------------------------------------------------------- 1 | /* 2 | LogUI Client Library 3 | Event Handlers / Scrolling Event 4 | 5 | A IIFE function yielding a scrolling event handler. 6 | 7 | @module: Scrolling Event Handler 8 | @author: David Maxwell 9 | @date: 2021-04-20 10 | */ 11 | 12 | import EventPackager from '../eventPackager'; 13 | import RequiredFeatures from '../required'; 14 | 15 | RequiredFeatures.addFeature('WeakMap'); 16 | 17 | const DELAY_TIME = 50; 18 | 19 | export default (function(root) { 20 | var _handler = {}; 21 | var tracking = null; 22 | var globalHandles = null; 23 | 24 | _handler.browserEvents = ['scroll']; 25 | 26 | _handler.init = function() { 27 | tracking = new WeakMap(); 28 | globalHandles = []; // Keep track of all handles for when this event handler is stopped. 29 | 30 | return; 31 | }; 32 | 33 | _handler.stop = function() { 34 | for (let handleID of globalHandles) { 35 | clearTimeout(handleID); 36 | } 37 | 38 | tracking = null; 39 | globalHandles = null; 40 | } 41 | 42 | _handler.logUIEventCallback = function(eventContext, browserEvent, trackingConfig) { 43 | let element = eventContext; 44 | 45 | // If tracking has the element in question, we know there's already a queue. So we should add to it. 46 | // We shouldn't log here, as the queue is non-zero. So we add to it, and simply return. 47 | if (tracking.has(element)) { 48 | let existing = tracking.get(element); 49 | 50 | let handle = (setTimeout(() => { 51 | endScrollEvent(element, handle); 52 | }, DELAY_TIME)); 53 | 54 | globalHandles.push(handle); 55 | 56 | existing['handles'].push(handle); 57 | tracking.set(element, existing); 58 | return; 59 | } 60 | 61 | // If we get here, we know that the element has not yet been tracked. 62 | // We can create the necessary data structure to track its scrolling interactions, and fire off an event for the start of scrolling. 63 | let handle = (setTimeout(() => { 64 | endScrollEvent(element, handle); 65 | }, DELAY_TIME)); 66 | 67 | globalHandles.push(handle); 68 | 69 | let mappedObject = { 70 | handles: [handle], 71 | eventContext: eventContext, 72 | trackingConfig: trackingConfig, 73 | }; 74 | 75 | tracking.set(element, mappedObject); 76 | 77 | let returnObject = { 78 | type: 'scrollStart', 79 | } 80 | 81 | let eventName = getEventName(trackingConfig, 'scrollStart') 82 | 83 | if (eventName) { 84 | returnObject['name'] = eventName; 85 | } 86 | 87 | return returnObject; 88 | } 89 | 90 | var endScrollEvent = function(element, handle) { 91 | if (tracking.has(element)) { 92 | let trackedElementDetails = tracking.get(element); 93 | let i = trackedElementDetails['handles'].indexOf(handle); 94 | 95 | // The timeout for the given handle has been met; remove it from the array of current timeout handles on this element. 96 | trackedElementDetails['handles'].splice(i, 1); 97 | 98 | // Make sure we remove the entry from globalHandles, too! 99 | // Re-use i here. We don't need it. 100 | i = globalHandles.indexOf(handle); 101 | globalHandles.splice(i, 1); 102 | 103 | // If the array has reached a length of zero, there are no more pending timeouts to remove. 104 | // In this instance, the scroll event has been completed; remove from the tracking WeakMap and tell the Event Packager to package up a scroll complete. 105 | // Using EventPackager here is J-A-N-K-Y -- in a future revision, it would be good to send it back to the EventCallbackHandler and tell that to package up. 106 | // This feels like I'm cheating a little bit :-( Future self: we need EventCallbackHandler to handle asynchronous events being fired in. 107 | if (trackedElementDetails['handles'].length == 0) { 108 | tracking.delete(element); 109 | 110 | let returnObject = { 111 | type: 'scrollEnd', 112 | } 113 | 114 | let eventName = getEventName(trackedElementDetails['trackingConfig'], 'scrollEnd') 115 | 116 | if (eventName) { 117 | returnObject['name'] = eventName; 118 | } 119 | 120 | // This is hacky. This should not be called here. This should be like an async callback to the EventCallbackHandler. 121 | EventPackager.packageInteractionEvent(element, returnObject, trackedElementDetails['trackingConfig']); 122 | } 123 | } 124 | 125 | return; 126 | } 127 | 128 | var getEventName = function(trackingConfig, eventName) { 129 | let trackingConfigProperties = trackingConfig.properties; 130 | 131 | if (trackingConfigProperties && 132 | trackingConfigProperties.hasOwnProperty(eventName) && 133 | trackingConfigProperties[eventName].hasOwnProperty('name')) { 134 | return trackingConfigProperties[eventName].name; 135 | } 136 | 137 | return undefined; 138 | } 139 | 140 | return _handler; 141 | })(window); -------------------------------------------------------------------------------- /src/modules/eventPackager.js: -------------------------------------------------------------------------------- 1 | /* 2 | LogUI Client Library 3 | Event Packager Module 4 | 5 | An IIFE function returning the predispatching phase of LogUI. 6 | Handles the collection of data from a variety of sources, packages it up into an object, and sends it to the dispatcher. 7 | 8 | @module: Event Packager Module 9 | @author: David Maxwell 10 | @date: 2021-02-24 11 | */ 12 | 13 | import Config from './config'; 14 | import MetadataHandler from './metadataHandler'; 15 | import Dispatcher from '__dispatcherImportInPackager__'; 16 | 17 | export default (function(root) { 18 | var _public = {}; 19 | 20 | _public.init = function() { 21 | return true; 22 | }; 23 | 24 | _public.stop = function() { }; 25 | 26 | _public.packageInteractionEvent = function(element, eventDetails, trackingConfig) { 27 | let packageObject = getBasicPackageObject(); 28 | 29 | packageObject.eventType = 'interactionEvent'; 30 | packageObject.eventDetails = eventDetails; 31 | packageObject.metadata = MetadataHandler.getMetadata(element, trackingConfig); 32 | 33 | Dispatcher.sendObject(packageObject); 34 | }; 35 | 36 | _public.packageCustomEvent = function(eventDetails) { 37 | let packageObject = getBasicPackageObject(); 38 | 39 | packageObject.eventType = 'customEvent'; 40 | packageObject.eventDetails = eventDetails; 41 | 42 | Dispatcher.sendObject(packageObject); 43 | } 44 | 45 | _public.packageBrowserEvent = function(eventDetails) { 46 | let packageObject = getBasicPackageObject(); 47 | 48 | packageObject.eventType = 'browserEvent'; 49 | packageObject.eventDetails = eventDetails; 50 | 51 | Dispatcher.sendObject(packageObject); 52 | }; 53 | 54 | _public.packageStatusEvent = function(eventDetails) { 55 | let packageObject = getBasicPackageObject(); 56 | 57 | packageObject.eventType = 'statusEvent'; 58 | packageObject.eventDetails = eventDetails; 59 | 60 | Dispatcher.sendObject(packageObject); 61 | }; 62 | 63 | var getBasicPackageObject = function() { 64 | let currentTimestamp = new Date(); 65 | let sessionStartTimestamp = Config.sessionData.getSessionStartTimestamp(); 66 | let libraryStartTimestamp = Config.sessionData.getLibraryStartTimestamp(); 67 | 68 | return { 69 | eventType: null, 70 | eventDetails: {}, 71 | sessionID: Config.sessionData.getSessionIDKey(), 72 | timestamps: { 73 | eventTimestamp: currentTimestamp, 74 | sinceSessionStartMillis: currentTimestamp - sessionStartTimestamp, 75 | sinceLogUILoadMillis: currentTimestamp - libraryStartTimestamp, 76 | }, 77 | applicationSpecificData: Config.applicationSpecificData.get(), 78 | } 79 | } 80 | 81 | return _public; 82 | })(window); -------------------------------------------------------------------------------- /src/modules/helpers.js: -------------------------------------------------------------------------------- 1 | /* 2 | LogUI Client Library 3 | Helpers Module 4 | 5 | A IIFE function containing several helper methods used throughout the rest of the LogUI client library. 6 | 7 | @module: Helpers 8 | @author: David Maxwell 9 | @date: 2020-09-14 10 | */ 11 | 12 | export default (function(root) { 13 | var _helpers = {}; 14 | 15 | _helpers.$ = root.document.querySelector.bind(root.document); 16 | _helpers.$$ = root.document.querySelectorAll.bind(root.document); 17 | 18 | _helpers.console = function(messageStr, currentState=null, isWarning=false) { 19 | let currentStateString = ''; 20 | let consoleFunction = console.log; 21 | 22 | if (currentState) { 23 | currentStateString = ` (${currentState})`; 24 | } 25 | 26 | if (isWarning) { 27 | consoleFunction = console.warn; 28 | } 29 | 30 | if (root.LogUI.Config.getConfigProperty('verbose') || isWarning) { 31 | var timeDelta = new Date().getTime() - root.LogUI.Config.getInitTimestamp(); 32 | 33 | if (typeof messageStr === 'object' && messageStr !== null) { 34 | consoleFunction(`LogUI${currentStateString} @ ${timeDelta}ms > Logged object below`); 35 | consoleFunction(messageStr); 36 | 37 | return; 38 | } 39 | 40 | consoleFunction(`LogUI${currentStateString} @ ${timeDelta}ms > ${messageStr}`); 41 | } 42 | }; 43 | 44 | _helpers.getElementDescendant = function(rootObject, descendantString=null, separator='.') { 45 | if (!descendantString || descendantString == []) { 46 | return rootObject; 47 | } 48 | 49 | let descendantSplitArray = descendantString.split(separator); 50 | while (descendantSplitArray.length && (rootObject = rootObject[descendantSplitArray.shift()])); 51 | 52 | return rootObject; 53 | }; 54 | 55 | _helpers.extendObject = function(objectA, objectB) { 56 | for (var key in objectB) { 57 | if (objectB.hasOwnProperty(key)) { 58 | objectA[key] = objectB[key]; 59 | } 60 | } 61 | 62 | return objectA; 63 | }; 64 | 65 | 66 | return _helpers; 67 | })(window); -------------------------------------------------------------------------------- /src/modules/metadataHandler.js: -------------------------------------------------------------------------------- 1 | /* 2 | LogUI Client Library 3 | Metadata Handler Module 4 | 5 | An IIFE function returning the handler for metadata for LogUI. 6 | Gathers data from various sources in preparation for sending to the dispatcher. 7 | 8 | @module: Metadata Handler Module 9 | @author: David Maxwell 10 | @date: 2021-03-05 11 | */ 12 | 13 | import Sourcers from './metadataSourcers/*'; 14 | 15 | export default (function(root) { 16 | var _public = {}; 17 | 18 | _public.init = function() { 19 | for (let sourcer in Sourcers) { 20 | Sourcers[sourcer].init(); 21 | } 22 | 23 | return true; 24 | }; 25 | 26 | _public.stop = function() { 27 | for (let sourcerName in Sourcers) { 28 | let sourcer = Sourcers[sourcerName]; 29 | 30 | if (sourcer.hasOwnProperty('stop')) { 31 | sourcer.stop(); 32 | } 33 | } 34 | }; 35 | 36 | _public.getMetadataValue = function(element, entryConfig) { 37 | let selectedSourcer = getSourcer(entryConfig.sourcer); 38 | 39 | if (!selectedSourcer) { 40 | return; 41 | } 42 | 43 | if (!entryConfig.hasOwnProperty('nameForLog') || !entryConfig.hasOwnProperty('lookFor')) { 44 | return; 45 | } 46 | 47 | return selectedSourcer.getObject(element, entryConfig); 48 | }; 49 | 50 | _public.getMetadata = function(element, trackingConfig) { 51 | let returnArray = []; 52 | let observedNames = []; 53 | 54 | if (trackingConfig.hasOwnProperty('metadata')) { 55 | for (let entry of trackingConfig.metadata) { 56 | let objectToPush = _public.getMetadataValue(element, entry); 57 | 58 | if (observedNames.includes(entry.nameForLog)) { 59 | continue; 60 | } 61 | 62 | if (objectToPush) { 63 | returnArray.push(objectToPush); 64 | observedNames.push(entry.nameForLog); 65 | } 66 | } 67 | } 68 | 69 | return returnArray; 70 | }; 71 | 72 | var getSourcer = function(requestedSourcerName) { 73 | for (let sourcerName in Sourcers) { 74 | if (sourcerName == requestedSourcerName) { 75 | return Sourcers[sourcerName]; 76 | } 77 | } 78 | 79 | return undefined; 80 | }; 81 | 82 | return _public; 83 | })(window); -------------------------------------------------------------------------------- /src/modules/metadataSourcers/elementAttribute.js: -------------------------------------------------------------------------------- 1 | /* 2 | LogUI Client Library 3 | Metadata Sourcers / DOM Element Attribute Sourcer 4 | 5 | An IIFE function yielding a module for extracting attribute values from a given DOM element. 6 | 7 | @module: DOM Element Attribute Sourcer Module 8 | @author: David Maxwell 9 | @date: 2021-03-05 10 | */ 11 | 12 | export default (function(root) { 13 | var _sourcer = {}; 14 | 15 | _sourcer.init = function() { 16 | }; 17 | 18 | _sourcer.stop = function() { 19 | 20 | }; 21 | 22 | _sourcer.getObject = function(element, request) { 23 | let value = _sourcer.getValue(element, request); 24 | 25 | if (value) { 26 | return { 27 | name: request.nameForLog, 28 | value: value, 29 | }; 30 | } 31 | 32 | return undefined; 33 | } 34 | 35 | _sourcer.getValue = function(element, request) { 36 | if (request.hasOwnProperty('lookFor')) { 37 | if (element.hasAttribute(request.lookFor)) { 38 | return element.getAttribute(request.lookFor); 39 | } 40 | } 41 | 42 | return undefined; 43 | } 44 | 45 | return _sourcer; 46 | })(window); -------------------------------------------------------------------------------- /src/modules/metadataSourcers/elementProperty.js: -------------------------------------------------------------------------------- 1 | /* 2 | LogUI Client Library 3 | Metadata Sourcers / DOM Element Property Sourcer 4 | 5 | An IIFE function yielding a module for extracting property values from a given DOM element. 6 | 7 | @module: DOM Element Property Sourcer Module 8 | @author: David Maxwell 9 | @date: 2021-03-25 10 | */ 11 | 12 | import Helpers from '../helpers'; 13 | 14 | export default (function(root) { 15 | var _sourcer = {}; 16 | 17 | _sourcer.init = function() { 18 | }; 19 | 20 | _sourcer.stop = function() { 21 | 22 | }; 23 | 24 | _sourcer.getObject = function(element, request) { 25 | let value = _sourcer.getValue(element, request); 26 | 27 | if (value) { 28 | return { 29 | name: request.nameForLog, 30 | value: value, 31 | }; 32 | } 33 | 34 | return undefined; 35 | } 36 | 37 | _sourcer.getValue = function(element, request) { 38 | if (request.hasOwnProperty('lookFor')) { 39 | 40 | if (request.hasOwnProperty('onElement')) { 41 | let selectedElement = Helpers.$(request.onElement); 42 | 43 | if (!selectedElement) { // The element specified does not exist in the DOM. 44 | return; 45 | } 46 | 47 | if (selectedElement[request.lookFor]) { 48 | return selectedElement[request.lookFor]; 49 | } 50 | 51 | return; 52 | } 53 | 54 | if (element[request.lookFor]) { 55 | return element[request.lookFor]; 56 | } 57 | } 58 | 59 | return undefined; 60 | } 61 | 62 | return _sourcer; 63 | })(window); -------------------------------------------------------------------------------- /src/modules/metadataSourcers/localStorage.js: -------------------------------------------------------------------------------- 1 | /* 2 | LogUI Client Library 3 | Metadata Sourcers / LocalStorage Sourcer 4 | 5 | An IIFE function yielding a module for extracting data from localStorage. 6 | 7 | @module: LocalStorage Sourcer Module 8 | @author: David Maxwell 9 | @date: 2021-03-05 10 | */ 11 | 12 | export default (function(root) { 13 | var _sourcer = {}; 14 | 15 | _sourcer.init = function() { 16 | 17 | }; 18 | 19 | _sourcer.stop = function() { 20 | 21 | }; 22 | 23 | _sourcer.getObject = function(element, request) { 24 | let value = _sourcer.getValue(element, request); 25 | 26 | if (value) { 27 | return { 28 | name: request.nameForLog, 29 | value: value, 30 | }; 31 | } 32 | 33 | return undefined; 34 | } 35 | 36 | _sourcer.getValue = function(element, request) { 37 | if (request.hasOwnProperty('lookFor')) { 38 | return localStorage.getItem(request.lookFor); 39 | } 40 | 41 | return undefined; 42 | } 43 | 44 | return _sourcer; 45 | })(window); -------------------------------------------------------------------------------- /src/modules/metadataSourcers/reactComponentProp.js: -------------------------------------------------------------------------------- 1 | /* 2 | LogUI Client Library 3 | Metadata Sourcers / React Component Prop Sourcer 4 | 5 | An IIFE function yielding a module for extracting prop data from a React component. 6 | 7 | @module: React Component Prop Sourcer Module 8 | @author: David Maxwell 9 | @date: 2021-03-05 10 | */ 11 | 12 | export default (function(root) { 13 | var _sourcer = {}; 14 | 15 | _sourcer.init = function() { 16 | 17 | }; 18 | 19 | _sourcer.stop = function() { 20 | 21 | }; 22 | 23 | _sourcer.getObject = function(element, request) { 24 | let value = _sourcer.getValue(element, request); 25 | 26 | if (value) { 27 | return { 28 | name: request.nameForLog, 29 | value: value, 30 | }; 31 | } 32 | 33 | return undefined; 34 | }; 35 | 36 | _sourcer.getValue = function(element, request) { 37 | for (let key in element) { 38 | if (key.startsWith('__reactFiber')) { 39 | let propsObject = element[key]._debugOwner.stateNode.props; 40 | 41 | if (propsObject.hasOwnProperty(request.lookFor)) { 42 | return propsObject[request.lookFor]; 43 | } 44 | } 45 | } 46 | 47 | return undefined; 48 | }; 49 | 50 | return _sourcer; 51 | })(window); -------------------------------------------------------------------------------- /src/modules/metadataSourcers/reactComponentState.js: -------------------------------------------------------------------------------- 1 | /* 2 | LogUI Client Library 3 | Metadata Sourcers / React Component State Sourcer 4 | 5 | An IIFE function yielding a module for extracting state data from a React component. 6 | 7 | @module: React Component State Sourcer Module 8 | @author: David Maxwell 9 | @date: 2021-03-05 10 | */ 11 | 12 | export default (function(root) { 13 | var _sourcer = {}; 14 | 15 | _sourcer.init = function() { 16 | 17 | }; 18 | 19 | _sourcer.stop = function() { 20 | 21 | }; 22 | 23 | _sourcer.getObject = function(element, request) { 24 | let value = _sourcer.getValue(element, request); 25 | 26 | if (value) { 27 | return { 28 | name: request.nameForLog, 29 | value: value, 30 | }; 31 | } 32 | 33 | return undefined; 34 | }; 35 | 36 | _sourcer.getValue = function(element, request) { 37 | for (let key in element) { 38 | if (key.startsWith('__reactFiber')) { 39 | let stateObject = element[key]._debugOwner.stateNode.state; 40 | 41 | if (stateObject.hasOwnProperty(request.lookFor)) { 42 | return stateObject[request.lookFor]; 43 | } 44 | } 45 | } 46 | 47 | return undefined; 48 | }; 49 | 50 | return _sourcer; 51 | })(window); -------------------------------------------------------------------------------- /src/modules/metadataSourcers/sessionStorage.js: -------------------------------------------------------------------------------- 1 | /* 2 | LogUI Client Library 3 | Metadata Sourcers / SessionStorage Sourcer 4 | 5 | An IIFE function yielding a module for extracting data from the SessionStorage object. 6 | 7 | @module: SessionStorage Sourcer Module 8 | @author: David Maxwell 9 | @date: 2021-03-05 10 | */ 11 | 12 | export default (function(root) { 13 | var _sourcer = {}; 14 | 15 | _sourcer.init = function() { 16 | 17 | }; 18 | 19 | _sourcer.stop = function() { 20 | 21 | }; 22 | 23 | _sourcer.getObject = function(element, request) { 24 | let value = _sourcer.getValue(element, request); 25 | 26 | if (value) { 27 | return { 28 | name: request.nameForLog, 29 | value: value, 30 | }; 31 | } 32 | 33 | return undefined; 34 | } 35 | 36 | _sourcer.getValue = function(element, request) { 37 | if (request.hasOwnProperty('lookFor')) { 38 | return sessionStorage.getItem(request.lookFor); 39 | } 40 | 41 | return undefined; 42 | } 43 | 44 | return _sourcer; 45 | })(window); -------------------------------------------------------------------------------- /src/modules/required.js: -------------------------------------------------------------------------------- 1 | /* 2 | LogUI Client Library 3 | Required Functionality Module 4 | 5 | An IIFE function providing a list of required functionality. 6 | Supports the addition of additional functionality for extensions. 7 | 8 | @module: Required Functionality Module 9 | @author: David Maxwell 10 | @date: 2020-10-06 11 | */ 12 | 13 | export default (function() { 14 | var _public = {}; 15 | var requiredFeatures = [ 16 | 'console', 17 | 'document', 18 | 'document.documentElement', 19 | 'document.querySelector', 20 | 'document.querySelectorAll', 21 | 'navigator', 22 | 'addEventListener', 23 | 'sessionStorage', 24 | 'MutationObserver', 25 | 'Number', 26 | 'WeakMap', 27 | 'Map', 28 | 'Date', 29 | 'Object', 30 | ]; 31 | 32 | _public.getFeatures = function() { 33 | return requiredFeatures; 34 | } 35 | 36 | _public.addFeature = function(feature) { 37 | requiredFeatures.push(feature); 38 | } 39 | 40 | return _public; 41 | })(); -------------------------------------------------------------------------------- /src/modules/specificFrameworkEvents.js: -------------------------------------------------------------------------------- 1 | /* 2 | LogUI Client Library 3 | Specific LogUI Framework Events Module 4 | 5 | An IIFE function returning functions for handling LogUI specific events. 6 | When fired, these functions gather the necessary data and send them to the relevant packager. 7 | 8 | @module: Specific LogUI Framework Events Module 9 | @author: David Maxwell 10 | @date: 2021-03-06 11 | */ 12 | 13 | import EventPackager from './eventPackager'; 14 | 15 | export default (function(root) { 16 | var _public = {}; 17 | 18 | _public.init = function() { 19 | root.addEventListener('logUIStarted', _public.logUIStartedEvent); 20 | return true; 21 | }; 22 | 23 | _public.stop = function() { 24 | root.removeEventListener('logUIStarted', _public.logUIStartedEvent); 25 | _public.logUIStoppedEvent(); 26 | }; 27 | 28 | _public.logUIStartedEvent = function() { 29 | let eventDetails = { 30 | type: 'started', 31 | browserAgentString: root.navigator.userAgent, 32 | screenResolution: { 33 | width: root.screen.width, 34 | height: root.screen.height, 35 | depth: root.screen.colorDepth, 36 | }, 37 | viewportResolution: { 38 | width: root.innerWidth, 39 | height: root.innerHeight, 40 | }, 41 | }; 42 | 43 | EventPackager.packageStatusEvent(eventDetails); 44 | }; 45 | 46 | _public.logUIStoppedEvent = function() { 47 | let eventDetails = { 48 | type: 'stopped', 49 | }; 50 | 51 | EventPackager.packageStatusEvent(eventDetails); 52 | }; 53 | 54 | _public.logUIUpdatedApplicationSpecificData = function() { 55 | EventPackager.packageStatusEvent({ 56 | type: 'applicationSpecificDataUpdated', 57 | }); 58 | }; 59 | 60 | return _public; 61 | })(window); -------------------------------------------------------------------------------- /src/modules/validationSchemas.js: -------------------------------------------------------------------------------- 1 | /* 2 | LogUI Client Library 3 | JSON Schema Validation Library 4 | 5 | An IIFE module providing access to JSON validation functionality. 6 | Also provides the ability to add additional properties to be validated. 7 | Useful for the dispatcher architecture. 8 | 9 | @module: ValidationSchemas 10 | @author: David Maxwell 11 | @date: 2020-09-20 12 | */ 13 | 14 | import { Validator } from 'jsonschema'; 15 | 16 | const SCHEMA_SUPPLIED_CONFIG = { 17 | id: 'LogUI-suppliedConfig', 18 | type: 'object', 19 | properties: { 20 | applicationSpecificData: {'type': 'object'}, 21 | logUIConfiguration: {'$ref': '/LogUI-logUIConfig'}, 22 | trackingConfiguration: {'$ref': '/LogUI-trackingConfig'}, 23 | }, 24 | required: [ 25 | 'applicationSpecificData', 26 | 'trackingConfiguration', 27 | 'logUIConfiguration' 28 | ] 29 | }; 30 | 31 | const SCHEMA_SUPPLIED_CONFIG_LOGUI = { 32 | id: 'LogUI-logUIConfig', 33 | type: 'object', 34 | properties: { 35 | verbose: {'type': 'boolean'}, 36 | browserEvents : {'$ref': '/LogUI-browserEvents'}, 37 | }, 38 | required: [], 39 | } 40 | 41 | const SCHEMA_SUPPLIED_CONFIG_LOGUI_BROWSEREVENTS = { 42 | id: 'LogUI-browserEvents', 43 | type: 'object', 44 | additionalProperties: false, 45 | properties: { 46 | blockEventBubbling: {'type': 'boolean'}, 47 | eventsWhileScrolling: {'type': 'boolean'}, 48 | URLChanges: {'type': 'boolean'}, 49 | contextMenu: {'type': 'boolean'}, 50 | pageFocus: {'type': 'boolean'}, 51 | trackCursor: {'type': 'boolean'}, 52 | cursorUpdateFrequency: {'type': 'number'}, 53 | cursorLeavingPage: {'type': 'boolean'}, 54 | pageResize: {'type': 'boolean'}, 55 | }, 56 | required: [], 57 | } 58 | 59 | const SCHEMA_SUPPLIED_TRACKING_CONFIG = { 60 | id: 'LogUI-trackingConfig', 61 | type: 'object', 62 | properties: { 63 | 64 | }, 65 | required: [ 66 | 67 | ] 68 | } 69 | 70 | export default (function(root) { 71 | var _public = {}; 72 | 73 | _public.addLogUIConfigProperty = function(propertyName, propertyType, isRequired=true) { 74 | SCHEMA_SUPPLIED_CONFIG_LOGUI.properties[propertyName] = {'type': propertyType}; 75 | 76 | if (isRequired) { 77 | SCHEMA_SUPPLIED_CONFIG_LOGUI.required.push(propertyName); 78 | } 79 | } 80 | 81 | _public.validateSuppliedConfigObject = function(suppliedConfigObject) { 82 | var suppliedConfigValidator = new Validator(); 83 | suppliedConfigValidator.addSchema(SCHEMA_SUPPLIED_CONFIG_LOGUI, '/LogUI-logUIConfig'); 84 | suppliedConfigValidator.addSchema(SCHEMA_SUPPLIED_TRACKING_CONFIG, '/LogUI-trackingConfig'); 85 | suppliedConfigValidator.addSchema(SCHEMA_SUPPLIED_CONFIG_LOGUI_BROWSEREVENTS, '/LogUI-browserEvents'); 86 | 87 | return suppliedConfigValidator.validate(suppliedConfigObject, SCHEMA_SUPPLIED_CONFIG); 88 | }; 89 | 90 | return _public; 91 | })(window); -------------------------------------------------------------------------------- /tests/env/landing.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | LogUI Test Environment 9 | 10 | 11 | 12 |

LogUI Test Environment

13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/modules/sample.spec.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | const expect = chai.expect; 3 | const http = require('http-server'); 4 | const playwright = require('playwright'); 5 | 6 | const HEADLESS = true; 7 | const BROWSER = 'chromium'; 8 | const SERVER_PORT = '8081'; 9 | const BASE_URL = `http://127.0.0.1:${SERVER_PORT}/env/landing.html`; 10 | const BASE_SCREENSHOTS = './tests/screenshots/'; 11 | 12 | let page, browser, context; 13 | 14 | let server = http.createServer({ 15 | root: './tests/', 16 | silent: true, 17 | }); 18 | 19 | describe('SUITE DESCRIPTION', () => { 20 | 21 | beforeEach(async() => { 22 | server.listen(SERVER_PORT); 23 | browser = await playwright[BROWSER].launch({headless: HEADLESS}); 24 | context = await browser.newContext(); 25 | page = await context.newPage(BASE_URL); 26 | 27 | await page.goto(BASE_URL); 28 | }); 29 | 30 | afterEach(async function() { 31 | await page.screenshot({path: `${BASE_SCREENSHOTS}sample.png`}); 32 | await browser.close(); 33 | server.close(); 34 | }); 35 | 36 | it('Example test (checking header tag innerHTML)', async() => { 37 | let headerElement = await page.$('h1'); 38 | let innerHTML = await headerElement.innerHTML(); 39 | 40 | expect(innerHTML).to.equal('LogUI Test Environment'); 41 | }); 42 | 43 | }); --------------------------------------------------------------------------------