├── README.md ├── server-side-example.go ├── honeycomb-fastly-logging.format ├── page-load.js ├── page-unload.js ├── LICENSE └── fastly-nav-timings.js /README.md: -------------------------------------------------------------------------------- 1 | # honeycombio-browser-js-example 2 | example code from https://www.honeycomb.io/blog/instrumenting-browser-page-loads-at-honeycomb/ 3 | -------------------------------------------------------------------------------- /server-side-example.go: -------------------------------------------------------------------------------- 1 | func (h *UserEventsHandler) sendToHoneycombAPI(eventType string, metadata map[string]interface{}, user *types.User) { 2 | ev := h.Libhoney.NewEvent() 3 | ev.Dataset = "user-events" // Name of the Honeycomb dataset we'll send these events to 4 | ev.AddField("type", eventType) // Name of the type of event, in our case either "page-load" or "page-unload" 5 | ev.Add(metadata) // All those event fields we constructed in the browser 6 | 7 | // And then we add some fields we have easy access to, because we know the 8 | // current user by their session: 9 | ev.AddField("user_id", user.ID) 10 | ev.AddField("user_email", user.Email) 11 | 12 | // Send the event to the Honeycomb API (goes to our internal Dogfood 13 | // Honeycomb cluster when called in Production). 14 | ev.Send() 15 | } 16 | -------------------------------------------------------------------------------- /honeycomb-fastly-logging.format: -------------------------------------------------------------------------------- 1 | { "time":"%{begin:%Y-%m-%dT%H:%M:%SZ}t", 2 | "data": { 3 | "geo_country_code":"%{client.geo.country_code}V", 4 | "server_datacenter":"%{server.datacenter}V", 5 | "client_as_number": %{client.as.number}V, 6 | "client_as_name": "%{json.escape(client.as.name)}V", 7 | "client_cwnd":%{client.socket.cwnd}V, 8 | "client_delivery_rate":%{client.socket.tcpi_delivery_rate}V, 9 | "client_retrans":%{client.socket.tcpi_delta_retrans}V, 10 | "client_rtt":%{client.socket.tcpi_rtt}V, 11 | "type": "%{subfield(req.body, "type", "&")}V", 12 | "page_load_id": %{subfield(req.body, "page_load_id", "&")}V, 13 | "page_url": "%{json.escape(subfield(req.body, "page_url", "&"))}V", 14 | "user_agent": "%{User-Agent}i", 15 | "window_height": %{subfield(req.body, "window_height", "&")}V, 16 | "window_width": %{subfield(req.body, "window_width", "&")}V, 17 | "screen_height": %{subfield(req.body, "screen_height", "&")}V, 18 | "screen_width": %{subfield(req.body, "screen_width", "&")}V, 19 | "timing_dns_end_ms": %{subfield(req.body, "timing_dns_end_ms", "&")}V, 20 | "timing_ssl_end_ms": %{subfield(req.body, "timing_ssl_end_ms", "&")}V, 21 | "timing_response_end_ms": %{subfield(req.body, "timing_response_end_ms", "&")}V, 22 | "timing_dom_interactive_ms": %{subfield(req.body, "timing_dom_interactive_ms", "&")}V, 23 | "timing_dom_complete_ms": %{subfield(req.body, "timing_dom_complete_ms", "&")}V, 24 | "timing_dom_loaded_ms": %{subfield(req.body, "timing_dom_loaded_ms", "&")}V, 25 | "timing_dns_duration_ms": %{subfield(req.body, "timing_dns_duration_ms", "&")}V, 26 | "timing_ssl_duration_ms": %{subfield(req.body, "timing_ssl_duration_ms", "&")}V, 27 | "timing_server_duration_ms": %{subfield(req.body, "timing_server_duration_ms", "&")}V, 28 | "timing_dom_loaded_duration_ms": %{subfield(req.body, "timing_dom_loaded_duration_ms", "&")}V, 29 | "timing_total_duration_ms": %{subfield(req.body, "timing_total_duration_ms", "&")}V, 30 | "redirect_count": %{subfield(req.body, "redirect_count", "&")}V, 31 | "resource_count": %{subfield(req.body, "resource_count", "&")}V 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /page-load.js: -------------------------------------------------------------------------------- 1 | // Send a user event to Honeycomb every time someone loads a page in the browser 2 | // so we can capture perf & device stats. 3 | // 4 | // Assumes the presence of `window`, `window.performance`, `window.navigator`, 5 | // and `window.performance.timing` objects 6 | import _ from "underscore"; 7 | import honeycomb from "../honeycomb"; 8 | 9 | // Randomly generate a page load ID so we can correlate load/unload events 10 | export let pageLoadId = Math.floor(Math.random() * 100000000); 11 | 12 | // Memory usage stats collected as soon as JS executes, so we can compare the 13 | // delta later on page unload 14 | export let jsHeapUsed = window.performance.memory && window.performance.memory.usedJSHeapSize; 15 | const jsHeapTotal = window.performance.memory && window.performance.memory.totalJSHeapSize; 16 | 17 | // Names of static asset files we care to collect metrics about 18 | const trackedAssets = ["/main.css", "/main.js"]; 19 | 20 | // Returns a very wide event of perf/client stats to send to Honeycomb 21 | const pageLoadEvent = function() { 22 | const nt = window.performance.timing; 23 | 24 | const event = { 25 | type: "page-load", 26 | page_load_id: pageLoadId, 27 | 28 | // User agent. We can parse the user agent into device, os name, os version, 29 | // browser name, and browser version fields server-side if we want to later. 30 | user_agent: window.navigator.userAgent, 31 | 32 | // Current window size & screen size stats 33 | // We use a derived column in Honeycomb to also be able to query window 34 | // total pixels and the ratio of window size to screen size. That way we 35 | // can understand whether users are making their window as large as they can 36 | // to try to fit Honeycomb content on screen, or whether they find a smaller 37 | // window size more comfortable. 38 | // 39 | // Capture how large the user has made their current window 40 | window_height: window.innerHeight, 41 | window_width: window.innerWidth, 42 | // Capture how large the user's entire screen is 43 | screen_height: window.screen && window.screen.height, 44 | screen_width: window.screen && window.screen.width, 45 | 46 | // The shape of the current url, similar to collecting rail's controller + 47 | // action, so we know which type of page the user was on. e.g. 48 | // "/:team_slug/datasets/:dataset_slug/triggers" 49 | // path_shape: document.querySelector('meta[name=goji-path]').content, 50 | 51 | // Chrome-only (for now) information on internet connection type (4g, wifi, etc.) 52 | // https://developers.google.com/web/updates/2017/10/nic62 53 | connection_type: navigator.connection && navigator.connection.type, 54 | connection_type_effective: navigator.connection && navigator.connection.effectiveType, 55 | connection_rtt: navigator.connection && navigator.connection.rtt, 56 | 57 | // Navigation (page load) timings, transformed from timestamps into deltas 58 | timing_unload_ms: nt.unloadEnd - nt.navigationStart, 59 | timing_dns_end_ms: nt.domainLookupEnd - nt.navigationStart, 60 | timing_ssl_end_ms: nt.connectEnd - nt.navigationStart, 61 | timing_response_end_ms: nt.responseEnd - nt.navigationStart, 62 | timing_dom_interactive_ms: nt.domInteractive - nt.navigationStart, 63 | timing_dom_complete_ms: nt.domComplete - nt.navigationStart, 64 | timing_dom_loaded_ms: nt.loadEventEnd - nt.navigationStart, 65 | timing_ms_first_paint: nt.msFirstPaint - nt.navigationStart, // Nonstandard IE/Edge-only first paint 66 | 67 | // Some calculated navigation timing durations, for easier graphing in Honeycomb 68 | // We could also use a derived column to do these calculations in the UI 69 | // from the above fields if we wanted to keep our event payload smaller. 70 | timing_dns_duration_ms: nt.domainLookupEnd - nt.domainLookupStart, 71 | timing_ssl_duration_ms: nt.connectEnd - nt.connectStart, 72 | timing_server_duration_ms: nt.responseEnd - nt.requestStart, 73 | timing_dom_loaded_duration_ms: nt.loadEventEnd - nt.domComplete, 74 | 75 | // Entire page load duration 76 | timing_total_duration_ms: nt.loadEventEnd - nt.connectStart, 77 | }; 78 | 79 | // First paint data via PerformancePaintTiming (Chrome only for now) 80 | const hasPerfTimeline = !!window.performance.getEntriesByType; 81 | if (hasPerfTimeline) { 82 | let paints = window.performance.getEntriesByType("paint"); 83 | 84 | // Loop through array of two PerformancePaintTimings and send both 85 | _.each(paints, function(paint) { 86 | if (paint.name === "first-paint") { 87 | event.timing_first_paint_ms = paint.startTime; 88 | } else if (paint.name === "first-contentful-paint") { 89 | event.timing_first_contentful_paint_ms = paint.startTime; 90 | } 91 | }); 92 | } 93 | 94 | // Redirect count (inconsistent browser support) 95 | // Find out if the user was redirected on their way to landing on this page, 96 | // so we can have visibility into whether redirects are slowing down the experience 97 | event.redirect_count = window.performance.navigation && window.performance.navigation.redirectCount; 98 | 99 | // Memory info (Chrome) — also send this on unload so we can compare heap size 100 | // and understand how much memory we're using as the user interacts with the page 101 | if (window.performance.memory) { 102 | event.js_heap_size_total_b = jsHeapTotal; 103 | event.js_heap_size_used_b = jsHeapUsed; 104 | } 105 | 106 | // ResourceTiming stats 107 | // We don't care about getting stats for every single static asset, but we do 108 | // care about the overall count (e.g. which pages could be slow because they 109 | // make a million asset requests?) and the sizes of key files (are we sending 110 | // our users massive js files that could slow down their experience? should we 111 | // be code-splitting for more manageable file sizes?). 112 | if (hasPerfTimeline) { 113 | let resources = window.performance.getEntriesByType("resource"); 114 | event.resource_count = resources.length; 115 | 116 | // Loop through resources looking for ones that match tracked asset names 117 | _.each(resources, function(resource) { 118 | const fileName = _.find(trackedAssets, fileName => resource.name.indexOf(fileName) > -1); 119 | if (fileName) { 120 | // Don't put chars like . and / in the key name 121 | const name = fileName.replace("/", "").replace(".", "_"); 122 | 123 | event[`resource_${name}_encoded_size_kb`] = resource.encodedBodySize; 124 | event[`resource_${name}_decoded_size_kb`] = resource.decodedBodySize; 125 | event[`resource_${name}_timing_duration_ms`] = resource.responseEnd - resource.startTime; 126 | } 127 | }); 128 | } 129 | 130 | return event; 131 | }; 132 | 133 | 134 | // Send this wide event we've constructed after the page has fully loaded 135 | window.addEventListener("load", function() { 136 | // Wait a tick so this all runs after any onload handlers 137 | setTimeout(function() { 138 | // Sends the event to our servers for forwarding on to api.honeycomb.io 139 | honeycomb.sendEvent(pageLoadEvent()); 140 | }, 0); 141 | }); 142 | -------------------------------------------------------------------------------- /page-unload.js: -------------------------------------------------------------------------------- 1 | // Send a user event to Honeycomb every time someone loads a page in the browser 2 | // so we can capture perf & device stats. 3 | // 4 | // Assumes the presence of `window`, `window.performance`, `window.navigator`, 5 | // and `window.performance.timing` objects 6 | import _ from "underscore"; 7 | import honeycomb from "../honeycomb"; 8 | 9 | // Randomly generate a page load ID so we can correlate load/unload events 10 | export let pageLoadId = Math.floor(Math.random() * 100000000); 11 | 12 | // Memory usage stats collected as soon as JS executes, so we can compare the 13 | // delta later on page unload 14 | export let jsHeapUsed = window.performance.memory && window.performance.memory.usedJSHeapSize; 15 | const jsHeapTotal = window.performance.memory && window.performance.memory.totalJSHeapSize; 16 | 17 | // Names of static asset files we care to collect metrics about 18 | const trackedAssets = ["/main.css", "/main.js"]; 19 | 20 | // Returns a very wide event of perf/client stats to send to Honeycomb 21 | const pageLoadEvent = function() { 22 | const nt = window.performance.timing; 23 | 24 | const event = { 25 | type: "page-load", 26 | page_load_id: pageLoadId, 27 | 28 | // User agent. We can parse the user agent into device, os name, os version, 29 | // browser name, and browser version fields server-side if we want to later. 30 | user_agent: window.navigator.userAgent, 31 | 32 | // Current window size & screen size stats 33 | // We use a derived column in Honeycomb to also be able to query window 34 | // total pixels and the ratio of window size to screen size. That way we 35 | // can understand whether users are making their window as large as they can 36 | // to try to fit Honeycomb content on screen, or whether they find a smaller 37 | // window size more comfortable. 38 | // 39 | // Capture how large the user has made their current window 40 | window_height: window.innerHeight, 41 | window_width: window.innerWidth, 42 | // Capture how large the user's entire screen is 43 | screen_height: window.screen && window.screen.height, 44 | screen_width: window.screen && window.screen.width, 45 | 46 | // The shape of the current url, similar to collecting rail's controller + 47 | // action, so we know which type of page the user was on. e.g. 48 | // "/:team_slug/datasets/:dataset_slug/triggers" 49 | path_shape: document.querySelector('meta[name=goji-path]').content, 50 | 51 | // Chrome-only (for now) information on internet connection type (4g, wifi, etc.) 52 | // https://developers.google.com/web/updates/2017/10/nic62 53 | connection_type: navigator.connection && navigator.connection.type, 54 | connection_type_effective: navigator.connection && navigator.connection.effectiveType, 55 | connection_rtt: navigator.connection && navigator.connection.rtt, 56 | 57 | // Navigation (page load) timings, transformed from timestamps into deltas 58 | timing_unload_ms: nt.unloadEnd - nt.navigationStart, 59 | timing_dns_end_ms: nt.domainLookupEnd - nt.navigationStart, 60 | timing_ssl_end_ms: nt.connectEnd - nt.navigationStart, 61 | timing_response_end_ms: nt.responseEnd - nt.navigationStart, 62 | timing_dom_interactive_ms: nt.domInteractive - nt.navigationStart, 63 | timing_dom_complete_ms: nt.domComplete - nt.navigationStart, 64 | timing_dom_loaded_ms: nt.loadEventEnd - nt.navigationStart, 65 | timing_ms_first_paint: nt.msFirstPaint - nt.navigationStart, // Nonstandard IE/Edge-only first paint 66 | 67 | // Some calculated navigation timing durations, for easier graphing in Honeycomb 68 | // We could also use a derived column to do these calculations in the UI 69 | // from the above fields if we wanted to keep our event payload smaller. 70 | timing_dns_duration_ms: nt.domainLookupEnd - nt.domainLookupStart, 71 | timing_ssl_duration_ms: nt.connectEnd - nt.connectStart, 72 | timing_server_duration_ms: nt.responseEnd - nt.requestStart, 73 | timing_dom_loaded_duration_ms: nt.loadEventEnd - nt.domComplete, 74 | 75 | // Entire page load duration 76 | timing_total_duration_ms: nt.loadEventEnd - nt.connectStart, 77 | }; 78 | 79 | // First paint data via PerformancePaintTiming (Chrome only for now) 80 | const hasPerfTimeline = !!window.performance.getEntriesByType; 81 | if (hasPerfTimeline) { 82 | let paints = window.performance.getEntriesByType("paint"); 83 | 84 | // Loop through array of two PerformancePaintTimings and send both 85 | _.each(paints, function(paint) { 86 | if (paint.name === "first-paint") { 87 | event.timing_first_paint_ms = paint.startTime; 88 | } else if (paint.name === "first-contentful-paint") { 89 | event.timing_first_contentful_paint_ms = paint.startTime; 90 | } 91 | }); 92 | } 93 | 94 | // Redirect count (inconsistent browser support) 95 | // Find out if the user was redirected on their way to landing on this page, 96 | // so we can have visibility into whether redirects are slowing down the experience 97 | event.redirect_count = window.performance.navigation && window.performance.navigation.redirectCount; 98 | 99 | // Memory info (Chrome) — also send this on unload so we can compare heap size 100 | // and understand how much memory we're using as the user interacts with the page 101 | if (window.performance.memory) { 102 | event.js_heap_size_total_b = jsHeapTotal; 103 | event.js_heap_size_used_b = jsHeapUsed; 104 | } 105 | 106 | // ResourceTiming stats 107 | // We don't care about getting stats for every single static asset, but we do 108 | // care about the overall count (e.g. which pages could be slow because they 109 | // make a million asset requests?) and the sizes of key files (are we sending 110 | // our users massive js files that could slow down their experience? should we 111 | // be code-splitting for more manageable file sizes?). 112 | if (hasPerfTimeline) { 113 | let resources = window.performance.getEntriesByType("resource"); 114 | event.resource_count = resources.length; 115 | 116 | // Loop through resources looking for ones that match tracked asset names 117 | _.each(resources, function(resource) { 118 | const fileName = _.find(trackedAssets, fileName => resource.name.indexOf(fileName) > -1); 119 | if (fileName) { 120 | // Don't put chars like . and / in the key name 121 | const name = fileName.replace("/", "").replace(".", "_"); 122 | 123 | event[`resource_${name}_encoded_size_kb`] = resource.encodedBodySize; 124 | event[`resource_${name}_decoded_size_kb`] = resource.decodedBodySize; 125 | event[`resource_${name}_timing_duration_ms`] = resource.responseEnd - resource.startTime; 126 | } 127 | }); 128 | } 129 | 130 | return event; 131 | }; 132 | 133 | 134 | // Send this wide event we've constructed after the page has fully loaded 135 | window.addEventListener("load", function() { 136 | // Wait a tick so this all runs after any onload handlers 137 | setTimeout(function() { 138 | // Sends the event to our servers for forwarding on to api.honeycomb.io 139 | honeycomb.sendEvent(pageLoadEvent()); 140 | }, 0); 141 | }); 142 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /fastly-nav-timings.js: -------------------------------------------------------------------------------- 1 | // Send a user event to Honeycomb every time someone loads a page in the browser 2 | // so we can capture perf & device stats. 3 | // 4 | // Assumes the presence of `window`, `window.performance`, `window.navigator`, 5 | // and `window.performance.timing` objects 6 | // Underscore.js 1.9.1 7 | // http://underscorejs.org 8 | // (c) 2009-2018 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 9 | // Underscore may be freely distributed under the MIT license. 10 | !function(){var n="object"==typeof self&&self.self===self&&self||"object"==typeof global&&global.global===global&&global||this||{},r=n._,e=Array.prototype,o=Object.prototype,s="undefined"!=typeof Symbol?Symbol.prototype:null,u=e.push,c=e.slice,p=o.toString,i=o.hasOwnProperty,t=Array.isArray,a=Object.keys,l=Object.create,f=function(){},h=function(n){return n instanceof h?n:this instanceof h?void(this._wrapped=n):new h(n)};"undefined"==typeof exports||exports.nodeType?n._=h:("undefined"!=typeof module&&!module.nodeType&&module.exports&&(exports=module.exports=h),exports._=h),h.VERSION="1.9.1";var v,y=function(u,i,n){if(void 0===i)return u;switch(null==n?3:n){case 1:return function(n){return u.call(i,n)};case 3:return function(n,r,t){return u.call(i,n,r,t)};case 4:return function(n,r,t,e){return u.call(i,n,r,t,e)}}return function(){return u.apply(i,arguments)}},d=function(n,r,t){return h.iteratee!==v?h.iteratee(n,r):null==n?h.identity:h.isFunction(n)?y(n,r,t):h.isObject(n)&&!h.isArray(n)?h.matcher(n):h.property(n)};h.iteratee=v=function(n,r){return d(n,r,1/0)};var g=function(u,i){return i=null==i?u.length-1:+i,function(){for(var n=Math.max(arguments.length-i,0),r=Array(n),t=0;t":">",'"':""","'":"'","`":"`"},P=h.invert(L),W=function(r){var t=function(n){return r[n]},n="(?:"+h.keys(r).join("|")+")",e=RegExp(n),u=RegExp(n,"g");return function(n){return n=null==n?"":""+n,e.test(n)?n.replace(u,t):n}};h.escape=W(L),h.unescape=W(P),h.result=function(n,r,t){h.isArray(r)||(r=[r]);var e=r.length;if(!e)return h.isFunction(t)?t.call(n):t;for(var u=0;u/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var J=/(.)^/,U={"'":"'","\\":"\\","\r":"r","\n":"n","\u2028":"u2028","\u2029":"u2029"},V=/\\|'|\r|\n|\u2028|\u2029/g,$=function(n){return"\\"+U[n]};h.template=function(i,n,r){!n&&r&&(n=r),n=h.defaults({},n,h.templateSettings);var t,e=RegExp([(n.escape||J).source,(n.interpolate||J).source,(n.evaluate||J).source].join("|")+"|$","g"),o=0,a="__p+='";i.replace(e,function(n,r,t,e,u){return a+=i.slice(o,u).replace(V,$),o=u+n.length,r?a+="'+\n((__t=("+r+"))==null?'':_.escape(__t))+\n'":t?a+="'+\n((__t=("+t+"))==null?'':__t)+\n'":e&&(a+="';\n"+e+"\n__p+='"),n}),a+="';\n",n.variable||(a="with(obj||{}){\n"+a+"}\n"),a="var __t,__p='',__j=Array.prototype.join,"+"print=function(){__p+=__j.call(arguments,'');};\n"+a+"return __p;\n";try{t=new Function(n.variable||"obj","_",a)}catch(n){throw n.source=a,n}var u=function(n){return t.call(this,n,h)},c=n.variable||"obj";return u.source="function("+c+"){\n"+a+"}",u},h.chain=function(n){var r=h(n);return r._chain=!0,r};var G=function(n,r){return n._chain?h(r).chain():r};h.mixin=function(t){return h.each(h.functions(t),function(n){var r=h[n]=t[n];h.prototype[n]=function(){var n=[this._wrapped];return u.apply(n,arguments),G(this,r.apply(h,n))}}),h},h.mixin(h),h.each(["pop","push","reverse","shift","sort","splice","unshift"],function(r){var t=e[r];h.prototype[r]=function(){var n=this._wrapped;return t.apply(n,arguments),"shift"!==r&&"splice"!==r||0!==n.length||delete n[0],G(this,n)}}),h.each(["concat","join","slice"],function(n){var r=e[n];h.prototype[n]=function(){return G(this,r.apply(this._wrapped,arguments))}}),h.prototype.value=function(){return this._wrapped},h.prototype.valueOf=h.prototype.toJSON=h.prototype.value,h.prototype.toString=function(){return String(this._wrapped)},"function"==typeof define&&define.amd&&define("underscore",[],function(){return h})}(); 11 | 12 | // Randomly generate a page load ID so we can correlate load/unload events 13 | let pageLoadId = Math.floor(Math.random() * 100000000); 14 | 15 | // Memory usage stats collected as soon as JS executes, so we can compare the 16 | // delta later on page unload 17 | let jsHeapUsed = window.performance.memory && window.performance.memory.usedJSHeapSize; 18 | const jsHeapTotal = window.performance.memory && window.performance.memory.totalJSHeapSize; 19 | 20 | // Names of static asset files we care to collect metrics about 21 | const trackedAssets = ["/main.css", "/main.js"]; 22 | 23 | // Returns a very wide event of perf/client stats to send to Honeycomb 24 | const pageLoadEvent = function() { 25 | const nt = window.performance.timing; 26 | 27 | const event = { 28 | type: "page-load", 29 | page_url: document.URL.replace(/#.*/, ""), 30 | page_load_id: pageLoadId, 31 | 32 | // User agent. We can parse the user agent into device, os name, os version, 33 | // browser name, and browser version fields server-side if we want to later. 34 | user_agent: window.navigator.userAgent, 35 | 36 | // Current window size & screen size stats 37 | // We use a derived column in Honeycomb to also be able to query window 38 | // total pixels and the ratio of window size to screen size. That way we 39 | // can understand whether users are making their window as large as they can 40 | // to try to fit Honeycomb content on screen, or whether they find a smaller 41 | // window size more comfortable. 42 | // 43 | // Capture how large the user has made their current window 44 | window_height: window.innerHeight, 45 | window_width: window.innerWidth, 46 | // Capture how large the user's entire screen is 47 | screen_height: window.screen && window.screen.height, 48 | screen_width: window.screen && window.screen.width, 49 | 50 | // Chrome-only (for now) information on internet connection type (4g, wifi, etc.) 51 | // https://developers.google.com/web/updates/2017/10/nic62 52 | connection_type: navigator.connection && navigator.connection.type, 53 | connection_type_effective: navigator.connection && navigator.connection.effectiveType, 54 | connection_rtt: navigator.connection && navigator.connection.rtt, 55 | 56 | // Navigation (page load) timings, transformed from timestamps into deltas 57 | timing_unload_ms: nt.unloadEnd - nt.navigationStart, 58 | timing_dns_end_ms: nt.domainLookupEnd - nt.navigationStart, 59 | timing_ssl_end_ms: nt.connectEnd - nt.navigationStart, 60 | timing_response_end_ms: nt.responseEnd - nt.navigationStart, 61 | timing_dom_interactive_ms: nt.domInteractive - nt.navigationStart, 62 | timing_dom_complete_ms: nt.domComplete - nt.navigationStart, 63 | timing_dom_loaded_ms: nt.loadEventEnd - nt.navigationStart, 64 | timing_ms_first_paint: nt.msFirstPaint - nt.navigationStart, // Nonstandard IE/Edge-only first paint 65 | 66 | // Some calculated navigation timing durations, for easier graphing in Honeycomb 67 | // We could also use a derived column to do these calculations in the UI 68 | // from the above fields if we wanted to keep our event payload smaller. 69 | timing_dns_duration_ms: nt.domainLookupEnd - nt.domainLookupStart, 70 | timing_ssl_duration_ms: nt.connectEnd - nt.connectStart, 71 | timing_server_duration_ms: nt.responseEnd - nt.requestStart, 72 | timing_dom_loaded_duration_ms: nt.loadEventEnd - nt.domComplete, 73 | 74 | // Entire page load duration 75 | timing_total_duration_ms: nt.loadEventEnd - nt.connectStart, 76 | }; 77 | 78 | // First paint data via PerformancePaintTiming (Chrome only for now) 79 | const hasPerfTimeline = !!window.performance.getEntriesByType; 80 | if (hasPerfTimeline) { 81 | let paints = window.performance.getEntriesByType("paint"); 82 | 83 | // Loop through array of two PerformancePaintTimings and send both 84 | _.each(paints, function(paint) { 85 | if (paint.name === "first-paint") { 86 | event.timing_first_paint_ms = paint.startTime; 87 | } else if (paint.name === "first-contentful-paint") { 88 | event.timing_first_contentful_paint_ms = paint.startTime; 89 | } 90 | }); 91 | } 92 | 93 | // Redirect count (inconsistent browser support) 94 | // Find out if the user was redirected on their way to landing on this page, 95 | // so we can have visibility into whether redirects are slowing down the experience 96 | event.redirect_count = window.performance.navigation && window.performance.navigation.redirectCount; 97 | 98 | // Memory info (Chrome) — also send this on unload so we can compare heap size 99 | // and understand how much memory we're using as the user interacts with the page 100 | if (window.performance.memory) { 101 | event.js_heap_size_total_b = jsHeapTotal; 102 | event.js_heap_size_used_b = jsHeapUsed; 103 | } 104 | 105 | // ResourceTiming stats 106 | // We don't care about getting stats for every single static asset, but we do 107 | // care about the overall count (e.g. which pages could be slow because they 108 | // make a million asset requests?) and the sizes of key files (are we sending 109 | // our users massive js files that could slow down their experience? should we 110 | // be code-splitting for more manageable file sizes?). 111 | if (hasPerfTimeline) { 112 | let resources = window.performance.getEntriesByType("resource"); 113 | event.resource_count = resources.length; 114 | 115 | // Loop through resources looking for ones that match tracked asset names 116 | _.each(resources, function(resource) { 117 | const fileName = _.find(trackedAssets, fileName => resource.name.indexOf(fileName) > -1); 118 | if (fileName) { 119 | // Don't put chars like . and / in the key name 120 | const name = fileName.replace("/", "").replace(".", "_"); 121 | 122 | event[`resource_${name}_encoded_size_kb`] = resource.encodedBodySize; 123 | event[`resource_${name}_decoded_size_kb`] = resource.decodedBodySize; 124 | event[`resource_${name}_timing_duration_ms`] = resource.responseEnd - resource.startTime; 125 | } 126 | }); 127 | } 128 | 129 | return event; 130 | }; 131 | 132 | 133 | // Send this wide event we've constructed after the page has fully loaded 134 | window.addEventListener("load", function() { 135 | // Wait a tick so this all runs after any onload handlers 136 | setTimeout(function() { 137 | // Sends the event to our servers for forwarding on to api.honeycomb.io 138 | let params = pageLoadEvent(); 139 | var queryString = Object.keys(params).map(key => key + '=' + params[key]).join('&'); 140 | navigator.sendBeacon("/.rum-beacon", queryString); 141 | }, 0); 142 | }); 143 | --------------------------------------------------------------------------------