Skip to content

Commit 25ff3d4

Browse files
committed
Add analytics-only adaptive toggle
1 parent 30e1370 commit 25ff3d4

3 files changed

Lines changed: 97 additions & 45 deletions

File tree

README.md

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ It also exposes this, and more, information via the `window.obs` object:
5555
```js
5656
{
5757
"config": {
58+
"adaptive": true,
5859
"observeChanges": false
5960
},
6061
"dataSaver": false,
@@ -111,22 +112,47 @@ This means you could do something like this:
111112

112113
## Installation
113114

114-
Obs.js **MUST** be placed in an inline `<script>` tag in the `<head>` of your
115-
document, before any other scripts, stylesheets, or HTML that may depend on it.
115+
There are two main options for installing Obs.js depending on whether you want
116+
fully adaptive mode, or analytics-only mode.
117+
118+
1. **Adaptive:** Must run early and inline; adds classes to `<html>` for CSS/JS
119+
adaptation later on.
120+
2. **Analytics:** Can be deferred/external; does not add classes to `<html>`, but still
121+
populates `window.obs` for analytics purposes.
122+
123+
### Adaptive Installation
124+
125+
If you are using Obs.js for adaptation, it **MUST** be placed in an inline
126+
`<script>` tag in the `<head>` of your document, before any other scripts,
127+
stylesheets, or HTML that may depend on it.
116128

117129
Copy/paste the following as close to the top of your `<head>` as possible:
118130

119131
```html
120132
<script>
121-
/*! Obs.js 0.2.1 | (c) Harry Roberts, csswizardry.com | MIT */
122-
;(()=>{const e=document.currentScript;if((!e||e.src||e.type&&"module"===e.type.toLowerCase())&&!1===/^(localhost|127\.0\.0\.1|::1)$/.test(location.hostname))return void console.warn("[Obs.js] Skipping: must be an inline, classic <script> in <head>.",e?e.src?"src="+e.src:"type="+e.type:"type=module");const t=document.documentElement,{connection:i}=navigator;window.obs=window.obs||{};const a=!0===(window.obs&&window.obs.config||{}).observeChanges,o=()=>{const e=window.obs||{},i="number"==typeof e.downlinkBucket?e.downlinkBucket:null;e.connectionCapability="low"===e.rttCategory&&null!=i&&i>=8?"strong":"high"===e.rttCategory||null!=i&&i<=5?"weak":"moderate";const a=!0===e.dataSaver||!0===e.batteryLow||!0===e.batteryCritical;e.conservationPreference=a?"conserve":"neutral";const o="weak"===e.connectionCapability||!0===e.dataSaver||!0===e.batteryCritical;e.deliveryMode="strong"!==e.connectionCapability||o||a?o?"lite":"cautious":"rich",e.canShowRichMedia="lite"!==e.deliveryMode,e.shouldAvoidRichMedia="lite"===e.deliveryMode,["strong","moderate","weak"].forEach(e=>{t.classList.remove(`has-connection-capability-${e}`)}),t.classList.add(`has-connection-capability-${e.connectionCapability}`),["conserve","neutral"].forEach(e=>{t.classList.remove(`has-conservation-preference-${e}`)}),t.classList.add(`has-conservation-preference-${e.conservationPreference}`),["rich","cautious","lite"].forEach(e=>{t.classList.remove(`has-delivery-mode-${e}`)}),t.classList.add(`has-delivery-mode-${e.deliveryMode}`)},n=()=>{if(!i)return;const{saveData:e,rtt:a,downlink:n}=i;window.obs.dataSaver=!!e,t.classList.toggle("has-data-saver",!!e);const s=(e=>Number.isFinite(e)?25*Math.ceil(e/25):null)(a);null!=s&&(window.obs.rttBucket=s);const r=(e=>Number.isFinite(e)?e<75?"low":e<=275?"medium":"high":null)(a);r&&(window.obs.rttCategory=r,["low","medium","high"].forEach(e=>t.classList.remove(`has-latency-${e}`)),t.classList.add(`has-latency-${r}`));const c=(l=n,Number.isFinite(l)?Math.ceil(l):null);var l;if(null!=c){window.obs.downlinkBucket=c;const e=c<=5?"low":c>=8?"high":"medium";window.obs.downlinkCategory=e,["low","medium","high"].forEach(e=>t.classList.remove(`has-bandwidth-${e}`)),t.classList.add(`has-bandwidth-${e}`)}"downlinkMax"in i&&(window.obs.downlinkMax=i.downlinkMax),o()};n(),a&&i&&"function"==typeof i.addEventListener&&i.addEventListener("change",n);const s=e=>{if(!e)return;const{level:i,charging:a}=e,n=Number.isFinite(i)?i<=.05:null;window.obs.batteryCritical=n;const s=Number.isFinite(i)?i<=.2:null;window.obs.batteryLow=s,["critical","low"].forEach(e=>t.classList.remove(`has-battery-${e}`)),s&&t.classList.add("has-battery-low"),n&&t.classList.add("has-battery-critical");const r=!!a;window.obs.batteryCharging=r,t.classList.toggle("has-battery-charging",r),o()};if("getBattery"in navigator&&navigator.getBattery().then(e=>{s(e),a&&"function"==typeof e.addEventListener&&(e.addEventListener("levelchange",()=>s(e)),e.addEventListener("chargingchange",()=>s(e)))}).catch(()=>{}),"deviceMemory"in navigator){const e=Number(navigator.deviceMemory),i=Number.isFinite(e)?e:null;window.obs.ramBucket=i;const a=(r=i,Number.isFinite(r)?r<=1?"very-low":r<=2?"low":r<=4?"medium":"high":null);a&&(window.obs.ramCategory=a,["very-low","low","medium","high"].forEach(e=>t.classList.remove(`has-ram-${e}`)),t.classList.add(`has-ram-${a}`))}var r;if("hardwareConcurrency"in navigator){const e=Number(navigator.hardwareConcurrency),i=Number.isFinite(e)?e:null;window.obs.cpuBucket=i;const a=(e=>Number.isFinite(e)?e<=2?"low":e<=5?"medium":"high":null)(i);a&&(window.obs.cpuCategory=a,["low","medium","high"].forEach(e=>t.classList.remove(`has-cpu-${e}`)),t.classList.add(`has-cpu-${a}`))}(()=>{const e=window.obs||{},i=e.ramCategory,a=e.cpuCategory;let o="moderate";"high"!==i&&"medium"!==i||"high"!==a?("very-low"===i||"low"===i||"low"===a)&&(o="weak"):o="strong",e.deviceCapability=o,["strong","moderate","weak"].forEach(e=>{t.classList.remove(`has-device-capability-${e}`)}),t.classList.add(`has-device-capability-${o}`)})()})();
123-
//# sourceURL=obs.inline.js
133+
/*! Obs.js | (c) Harry Roberts, csswizardry.com | MIT */
134+
;(()=>{const e=document.currentScript,i=window.obs&&window.obs.config||{},n=!1!==i.adaptive;if(n&&(!e||e.src||e.type&&"module"===e.type.toLowerCase())&&!1===/^(localhost|127\.0\.0\.1|::1)$/.test(location.hostname))return void console.warn("[Obs.js] Skipping: must be an inline, classic <script> in <head>.",e?e.src?"src="+e.src:"type="+e.type:"type=module");const t=document.documentElement,{connection:o}=navigator;window.obs=window.obs||{};const a=!0===i.observeChanges,r=e=>{n&&e.forEach(e=>t.classList.remove(e))},c=e=>{n&&t.classList.add(e)},s=(e,i)=>{n&&t.classList.toggle(e,i)};let l=!1;const d=()=>{const e=window.obs||{},i="number"==typeof e.downlinkBucket?e.downlinkBucket:null;e.connectionCapability="low"===e.rttCategory&&null!=i&&i>=8?"strong":"high"===e.rttCategory||null!=i&&i<=5?"weak":"moderate";const n=!0===e.dataSaver||!0===e.batteryLow||!0===e.batteryCritical;e.conservationPreference=n?"conserve":"neutral";const t="weak"===e.connectionCapability||!0===e.dataSaver||!0===e.batteryCritical;e.deliveryMode="strong"!==e.connectionCapability||t||n?t?"lite":"cautious":"rich",e.canShowRichMedia="lite"!==e.deliveryMode,e.shouldAvoidRichMedia="lite"===e.deliveryMode,r(["strong","moderate","weak"].map(e=>`has-connection-capability-${e}`)),c(`has-connection-capability-${e.connectionCapability}`),r(["conserve","neutral"].map(e=>`has-conservation-preference-${e}`)),c(`has-conservation-preference-${e.conservationPreference}`),r(["rich","cautious","lite"].map(e=>`has-delivery-mode-${e}`)),c(`has-delivery-mode-${e.deliveryMode}`)},w=()=>{if(!o)return;const{saveData:e,rtt:i,downlink:n}=o;window.obs.dataSaver=!!e,s("has-data-saver",!!e);const t=(e=>Number.isFinite(e)?25*Math.ceil(e/25):null)(i);null!=t&&(window.obs.rttBucket=t);const a=(e=>Number.isFinite(e)?e<75?"low":e<=275?"medium":"high":null)(i);a&&(window.obs.rttCategory=a,r(["low","medium","high"].map(e=>`has-latency-${e}`)),c(`has-latency-${a}`));const l=(w=n,Number.isFinite(w)?Math.ceil(w):null);var w;if(null!=l){window.obs.downlinkBucket=l;const e=l<=5?"low":l>=8?"high":"medium";window.obs.downlinkCategory=e,r(["low","medium","high"].map(e=>`has-bandwidth-${e}`)),c(`has-bandwidth-${e}`)}"downlinkMax"in o&&(window.obs.downlinkMax=o.downlinkMax),d()},u=e=>{if(!e)return;const{level:i,charging:n}=e,t=Number.isFinite(i)?i<=.05:null;window.obs.batteryCritical=t;const o=Number.isFinite(i)?i<=.2:null;window.obs.batteryLow=o,r(["critical","low"].map(e=>`has-battery-${e}`)),o&&c("has-battery-low"),t&&c("has-battery-critical");const a=!!n;window.obs.batteryCharging=a,s("has-battery-charging",a),d()},h=()=>{if(!l){if(l=!0,w(),a&&o&&"function"==typeof o.addEventListener&&o.addEventListener("change",w),"getBattery"in navigator&&navigator.getBattery().then(e=>{u(e),a&&"function"==typeof e.addEventListener&&(e.addEventListener("levelchange",()=>u(e)),e.addEventListener("chargingchange",()=>u(e)))}).catch(()=>{}),"deviceMemory"in navigator){const i=Number(navigator.deviceMemory),n=Number.isFinite(i)?i:null;window.obs.ramBucket=n;const t=(e=n,Number.isFinite(e)?e<=1?"very-low":e<=2?"low":e<=4?"medium":"high":null);t&&(window.obs.ramCategory=t,r(["very-low","low","medium","high"].map(e=>`has-ram-${e}`)),c(`has-ram-${t}`))}var e;if("hardwareConcurrency"in navigator){const e=Number(navigator.hardwareConcurrency),i=Number.isFinite(e)?e:null;window.obs.cpuBucket=i;const n=(e=>Number.isFinite(e)?e<=2?"low":e<=5?"medium":"high":null)(i);n&&(window.obs.cpuCategory=n,r(["low","medium","high"].map(e=>`has-cpu-${e}`)),c(`has-cpu-${n}`))}(()=>{const e=window.obs||{},i=e.ramCategory,n=e.cpuCategory;let t="moderate";"high"!==i&&"medium"!==i||"high"!==n?("very-low"===i||"low"===i||"low"===n)&&(t="weak"):t="strong",e.deviceCapability=t,r(["strong","moderate","weak"].map(e=>`has-device-capability-${e}`)),c(`has-device-capability-${t}`)})()}};if("prerendering"in document&&!0===document.prerendering){const e=()=>{document.removeEventListener("visibilitychange",i),h()},i=()=>{"visible"===document.visibilityState&&e()};document.addEventListener("prerenderingchange",e,{once:!0}),document.addEventListener("visibilitychange",i)}else h()})();
135+
//# sourceURL=obs.inline.js
124136
</script>
125137
```
126138

127139
Or download the [latest minified
128140
version](https://github.com/csswizardry/Obs.js/releases/latest).
129141

142+
### Analytics Installation
143+
144+
If you only want to collect signals for analytics, disable adaptive mode
145+
before Obs.js runs:
146+
147+
```html
148+
<script>window.obs = { config: { adaptive: false } };</script>
149+
<script src="/path/to/obs.js"></script>
150+
```
151+
152+
With `adaptive: false`, Obs.js still populates `window.obs`, but it will not
153+
add classes to the `<html>` element or require the strict inline-in-`<head>`
154+
installation pattern.
155+
130156
### Listen for Changes
131157

132158
If you have long-lived pages or a single-page app, you can instruct Obs.js to
@@ -200,6 +226,7 @@ Obs.js also stores the following properties on the `window.obs` object:
200226

201227
| Property | Type | Meaning | Computed/derived from | Notes |
202228
| ------------------------ | ------------------------------------------------- | -------------------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
229+
| `config.adaptive` | boolean | Enable adaptive HTML classes | **Default `true`**; set by you _before_ Obs.js runs | Set to `false` for analytics-only usage: Obs.js still populates `window.obs`, but it won’t mutate `<html>` or require inline `<head>` installation |
203230
| `config.observeChanges` | boolean | Attach change listeners | **Default `false`**; set by you _before_ Obs.js runs | Opt-in for SPAs or long-lived pages |
204231
| `dataSaver` | boolean | User enabled Data Saver | `navigator.connection.saveData` ||
205232
| `rttBucket` | number (ms) | RTT bucketed to **ceil** 25ms | `navigator.connection.rtt` | Undefined if Connection API missing |

0 commit comments

Comments
 (0)