diff --git a/README.md b/README.md
index 0eea20e14a45f1325c87cc02c6498ac695571b1b..59a1fcfc48d07d2a01ba2032fece8a77cc4ebd20 100644
--- a/README.md
+++ b/README.md
@@ -10,9 +10,16 @@ This is a very lightweight Speedtest implemented in Javascript, using XMLHttpReq
 ## Compatibility
 Only modern browsers are supported (IE11, latest Edge, latest Chrome, latest Firefox, latest Safari)
 
+## Features
+* Download
+* Upload
+* Ping
+* Jitter
+* IP Address
+* Telemetry (optional)
+
 ## Requirements
- - A reasonably fast web server. PHP is optional but recommended (see doc.md for details)
- - Some way to generate garbage data (PHP script included, see doc.md for other solutions)
+ - A reasonably fast web server with PHP (see doc.md for details and use without PHP)
  - Your server must accept large POST requests (up to 20 Megabytes), otherwise the upload test will fail
  - It's also better if your server does not use compression, but it's not mandatory
 
diff --git a/doc.md b/doc.md
index 6b8863b03569ee0e5678e59b9adb993eda209a12..e35a5361c8a0fa0c98d3faf2029341ceed48322c 100644
--- a/doc.md
+++ b/doc.md
@@ -1,7 +1,7 @@
 # HTML5 Speedtest
 
 > by Federico Dossena  
-> Version 4.2.9, July 19 2017  
+> Version 4.3, August 24 2017  
 > [https://github.com/adolfintel/speedtest/](https://github.com/adolfintel/speedtest/)
 
 
@@ -35,14 +35,16 @@ To install the test on your server, upload the following files:
 * `garbage.php`
 * `getIP.php`
 * `empty.php`
+* one of the examples
 
-You may also want to upload one of the examples to test it.  
-Later we'll see how to use the test without PHP.
+Later we'll see how to use the test without PHP, and how to configure the telemetry if you want to use it.
 
 __Important:__ keep all the files together; all paths are relative to the js file
 
 
 ## Usage
+You can modify one of the examples or start from scratch. If you are just editing one of the example, skip to the "test parameters" section.
+
 To run the test, you need to do 3 things:
 
 * Create the worker
@@ -246,15 +248,49 @@ You need to start the test with your replacements like this:
 ```js
 w.postMessage('start {"url_dl": "newGarbageURL", "url_ul": "newEmptyURL", "url_ping": "newEmptyURL", "url_getIp": "newIpURL"}')
 ```
+## Telemetry
+Telemetry currently requires PHP and MySQL.  
+To set up the telemetry, we need to do 4 things:
+* copy `telemetry.php`
+* edit `telemetry.php` to add your database access credentials
+* create the database
+* enable telemetry
+
+### Creating the database
+At the moment, only MySQL is supported.  
+Log into your database using phpMyAdmin or a similar software and import `telemetry.sql` into an empty database.  
+If you see a table called `speedtest_users`, empty, you did it right.
+
+### Configuring `telemetry.php`
+Open telemetry.php with notepad or a similar text editor, and insert your database access credentials
+```
+$MySql_username="USERNAME"; //your database username
+$MySql_password="PASSWORD"; //your database password
+$MySql_hostname="DB_HOSTNAME"; //database address, usually localhost\
+$MySql_databasename="DB_NAME"; //the name of the database where you loaded telemetry.sql
+```
+
+### Enabling telemetry
+Edit your test page; where you start the worker, you need to specify the `telemetry_level`.  
+There are 3 levels:
+* `none`: telemetry is disabled (default)
+* `basic`: telemetry collects IP, User Agent, Preferred language, Test results
+* `full`: same as above, but also collects a log (10-150 Kb each, not recommended)
+
+Example:
+```
+w.postMessage('start {"telemetry_level":"basic"}')
+```
+
+Also, see example8_telemetry.html
 
+### See the results
+At the moment there is no front-end to see the telemetry data; you can connect to the database and see the collected results in the `speedtest_users` table.
 
 ## Known bugs and limitations
 * The ping/jitter test is measured by seeing how long it takes for an empty XHR to complete. It is not an acutal ICMP ping
-* __Chrome:__ high CPU usage from XHR requests with very fast connections (like gigabit).
-  For this reason, the test may report inaccurate results if your CPU is too slow. (Does not affect most computers)
-* __IE11:__ the upload test is not precise on very fast connections
+* __IE11, Edge:__ the upload test is not precise on very fast connections
 * __IE11:__ the upload test may not work over HTTPS
-* __Safari:__ works, but needs more testing and tweaking for very fast connections
 * __Firefox:__ on some Linux systems with hardware acceleration turned off, the page rendering makes the browser lag, reducing the accuracy of the ping/jitter test
 
 ## Making changes
diff --git a/example8_telemetry.html b/example8_telemetry.html
new file mode 100644
index 0000000000000000000000000000000000000000..579035d95ba7bc4f97f88b92187c3596cb011ea2
--- /dev/null
+++ b/example8_telemetry.html
@@ -0,0 +1,105 @@
+<!doctype html>
+<html>
+<head>
+    <meta charset="utf-8" />
+    <meta name="referrer" content="no-referrer" />
+    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, user-scalable=no" />
+    <title>Speedtest</title>
+
+    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css" rel="stylesheet" />
+    <style type="text/css">
+        .st-block {
+            text-align: center;
+        }
+        .st-btn {
+            margin-top: -0.5rem;
+            margin-left: 1.5rem;
+        }
+        .st-value>span:empty::before {
+            content: "0.00";
+            color: #636c72;
+        }
+        #st-ip:empty::before {
+            content: "___.___.___.___";
+            color: #636c72;
+        }
+    </style>
+</head>
+
+<body class="my-4">
+    <div class="container">
+        <div class="row">
+            <div class="col-sm-12 mb-3">
+                <p class="h1">
+                    Speedtest
+                    <button id="st-start" class="btn btn-outline-primary st-btn" onclick="startTest()">Start</button>
+                    <button id="st-stop" class="btn btn-danger st-btn" onclick="stopTest()" hidden="true">Stop</button>
+					<p>Basic telemetry is active; results will be saved in your database. If the results don't appear, check the settings in telemetry.php</p>
+                </p>
+                <p class="lead">
+                    Your IP: <span id="st-ip"></span>
+                </p>
+            </div>
+            <div class="col-lg-3 col-md-6 mb-3 st-block">
+                <h3>Download</h3>
+                <p class="display-4 st-value"><span id="st-download"></span></p>
+                <p class="lead">Mbit/s</p>
+            </div>
+            <div class="col-lg-3 col-md-6 mb-3 st-block">
+                <h3>Upload</h3>
+                <p class="display-4 st-value"><span id="st-upload"></span></p>
+                <p class="lead">Mbit/s</p>
+            </div>
+            <div class="col-lg-3 col-md-6 mb-3 st-block">
+                <h3>Ping</h3>
+                <p class="display-4 st-value"><span id="st-ping"></span></p>
+                <p class="lead">ms</p>
+            </div>
+            <div class="col-lg-3 col-md-6 mb-3 st-block">
+                <h3>Jitter</h3>
+                <p class="display-4 st-value"><span id="st-jitter"></span></p>
+                <p class="lead">ms</p>
+            </div>
+        </div>
+    </div>
+
+    <script type="text/javascript">
+        var worker = null
+        function startTest() {
+            document.getElementById('st-start').hidden = true
+            document.getElementById('st-stop').hidden = false
+            worker = new Worker('speedtest_worker.js')
+            var interval = setInterval(function () { worker.postMessage('status') }, 100)
+            worker.onmessage = function (event) {
+                var download = document.getElementById('st-download')
+                var upload = document.getElementById('st-upload')
+                var ping = document.getElementById('st-ping')
+                var jitter = document.getElementById('st-jitter')
+                var ip = document.getElementById('st-ip')
+
+                var data = event.data.split(';')
+                var status = Number(data[0])
+                if (status >= 4) {
+                    clearInterval(interval)
+                    document.getElementById('st-start').hidden = false
+                    document.getElementById('st-stop').hidden = true
+                    w = null
+                }
+                if (status === 5) {
+                    // speedtest cancelled, clear output data
+                    data = []
+                }
+                download.textContent = (status==1&&data[1]==0)?"Starting":data[1]
+                upload.textContent = (status==3&&data[2]==0)?"Starting":data[2]
+                ping.textContent = data[3]
+                ip.textContent = data[4]
+                jitter.textContent = data[5]
+            }
+            worker.postMessage('start {"telemetry_level":"basic"}')
+        }
+        function stopTest() {
+            if (worker) worker.postMessage('abort')
+        }
+    </script>
+</body>
+</html>
diff --git a/speedtest_worker.js b/speedtest_worker.js
index 5c0c70995b006778112273b250ae70a574e7ae18..e263547d5f334c64d6d0bb92bbc4e5cb7f8a6aad 100644
--- a/speedtest_worker.js
+++ b/speedtest_worker.js
@@ -1,5 +1,5 @@
 /*
-	HTML5 Speedtest v4.2.8
+	HTML5 Speedtest v4.3
 	by Federico Dossena
 	https://github.com/adolfintel/speedtest/
 	GNU LGPLv3 License
@@ -13,6 +13,10 @@ var pingStatus = '' // ping in milliseconds with 2 decimal digits
 var jitterStatus = '' // jitter in milliseconds with 2 decimal digits
 var clientIp = '' // client's IP address as reported by getIP.php
 
+var log="" //telemetry log
+function tlog(s){log+=Date.now()+": "+s+"\n"}
+function twarn(s){log+=Date.now()+" WARN: "+s+"\n"; console.warn(s);}
+
 // test settings. can be overridden by sending specific values with the start command
 var settings = {
   time_ul: 15, // duration of upload test in seconds
@@ -30,7 +34,9 @@ var settings = {
   xhr_dlUseBlob: false, // if set to true, it reduces ram usage but uses the hard drive (useful with large garbagePhp_chunkSize and/or high xhr_dlMultistream)
   garbagePhp_chunkSize: 20, // size of chunks sent by garbage.php (can be different if enable_quirks is active)
   enable_quirks: true, // enable quirks for specific browsers. currently it overrides settings to optimize for specific browsers, unless they are already being overridden with the start command
-  overheadCompensationFactor: 1048576/925000 //compensation for HTTP+TCP+IP+ETH overhead. 925000 is how much data is actually carried over 1048576 (1mb) bytes downloaded/uploaded. This default value assumes HTTP+TCP+IPv4+ETH with typical MTUs over the Internet. You may want to change this if you're going through your local network with a different MTU or if you're going over IPv6 (see doc.md for some other values)
+  overheadCompensationFactor: 1048576/925000, //compensation for HTTP+TCP+IP+ETH overhead. 925000 is how much data is actually carried over 1048576 (1mb) bytes downloaded/uploaded. This default value assumes HTTP+TCP+IPv4+ETH with typical MTUs over the Internet. You may want to change this if you're going through your local network with a different MTU or if you're going over IPv6 (see doc.md for some other values)
+  telemetry_level: 0, // 0=disabled, 1=basic (results only), 2=full (results+log)
+  url_telemetry: 'telemetry.php' // path to the script that adds telemetry data to the database
 }
 
 var xhr = null // array of currently active xhr requests
@@ -88,13 +94,13 @@ this.addEventListener('message', function (e) {
         if (/Edge.(\d+\.\d+)/i.test(ua)) {
           // edge more precise with 3 download streams
           settings.xhr_dlMultistream = 3
+          settings.forceIE11Workaround = true //Edge 15 introduced a bug that causes onprogress events to not get fired, so for Edge, we have to use the "small chunks" workaround that reduces accuracy
         }
         if (/Chrome.(\d+)/i.test(ua) && (!!self.fetch)) {
           // chrome more precise with 5 streams
           settings.xhr_dlMultistream = 5
         }
       }
-
       if (typeof s.count_ping !== 'undefined') settings.count_ping = s.count_ping // number of pings for ping test
       if (typeof s.xhr_dlMultistream !== 'undefined') settings.xhr_dlMultistream = s.xhr_dlMultistream // number of download streams
       if (typeof s.xhr_ulMultistream !== 'undefined') settings.xhr_ulMultistream = s.xhr_ulMultistream // number of upload streams
@@ -104,19 +110,24 @@ this.addEventListener('message', function (e) {
       if (typeof s.time_dlGraceTime !== 'undefined') settings.time_dlGraceTime = s.time_dlGraceTime // dl test grace time before measuring
       if (typeof s.time_ulGraceTime !== 'undefined') settings.time_ulGraceTime = s.time_ulGraceTime // ul test grace time before measuring
       if (typeof s.overheadCompensationFactor !== 'undefined') settings.overheadCompensationFactor = s.overheadCompensationFactor //custom overhead compensation factor (default assumes HTTP+TCP+IP+ETH with typical MTUs)
-    } catch (e) { console.warn("Possible error in custom test settings. Some settings may not be applied. Exception: "+e) }
+	  if (typeof s.telemetry_level !== 'undefined') settings.telemetry_level = s.telemetry_level === "basic" ? 1 : s.telemetry_level === "full" ? 2 : 0; // telemetry level
+      if (typeof s.url_telemetry !== 'undefined') settings.url_telemetry = s.url_telemetry // url to telemetry.php
+    } catch (e) { twarn("Possible error in custom test settings. Some settings may not be applied. Exception: "+e) }
     // run the tests
-    console.log(settings)
-    getIp(function () { dlTest(function () { testStatus = 2; pingTest(function () { testStatus = 3; ulTest(function () { testStatus = 4 }) }) }) })
+    tlog(JSON.stringify(settings))
+    getIp(function () { dlTest(function () { testStatus = 2; pingTest(function () { testStatus = 3; ulTest(function () { testStatus = 4; sendTelemetry() }) }) }) })
   }
   if (params[0] === 'abort') { // abort command
+    tlog("manually aborted");
     clearRequests() // stop all xhr activity
     if (interval) clearInterval(interval) // clear timer if present
-    testStatus = 5; dlStatus = ''; ulStatus = ''; pingStatus = ''; jitterStatus = '' // set test as aborted
+    if (settings.telemetry_level > 1) sendTelemetry()
+	testStatus = 5; dlStatus = ''; ulStatus = ''; pingStatus = ''; jitterStatus = '' // set test as aborted
   }
 })
 // stops all XHR activity, aggressively
 function clearRequests () {
+  tlog("stopping pending XHRs");
   if (xhr) {
     for (var i = 0; i < xhr.length; i++) {
       if (useFetchAPI) try { xhr[i].cancelRequested = true } catch (e) { }
@@ -130,13 +141,16 @@ function clearRequests () {
 }
 // gets client's IP using url_getIp, then calls the done function
 function getIp (done) {
+  tlog("getIp");
   if (settings.url_getIp == "-1") {done(); return}
   xhr = new XMLHttpRequest()
   xhr.onload = function () {
+	tlog("IP: "+xhr.responseText);
     clientIp = xhr.responseText
     done()
   }
   xhr.onerror = function () {
+	tlog("getIp failed");
     done()
   }
   xhr.open('GET', settings.url_getIp + url_sep(settings.url_getIp) + 'r=' + Math.random(), true)
@@ -145,6 +159,7 @@ function getIp (done) {
 // download test, calls done function when it's over
 var dlCalled = false // used to prevent multiple accidental calls to dlTest
 function dlTest (done) {
+  tlog("dlTest")
   if (dlCalled) return; else dlCalled = true // dlTest already called?
   if (settings.url_dl == "-1") {done(); return}
   var totLoaded = 0.0, // total number of loaded bytes
@@ -156,10 +171,12 @@ function dlTest (done) {
   var testStream = function (i, delay) {
     setTimeout(function () {
       if (testStatus !== 1) return // delayed stream ended up starting after the end of the download test
+	  tlog("dl test stream started "+i+" "+delay)
       var prevLoaded = 0 // number of bytes loaded last time onprogress was called
       var x = new XMLHttpRequest()
       xhr[i] = x
       xhr[i].onprogress = function (event) {
+		tlog("dl stream progress event "+i+" "+event.loaded);
         if (testStatus !== 1) { try { x.abort() } catch (e) { } } // just in case this XHR is still running after the download test
         // progress event, add number of new loaded bytes to totLoaded
         var loadDiff = event.loaded <= 0 ? 0 : (event.loaded - prevLoaded)
@@ -169,11 +186,13 @@ function dlTest (done) {
       }.bind(this)
       xhr[i].onload = function () {
         // the large file has been loaded entirely, start again
+		tlog("dl stream finished "+i)
         try { xhr[i].abort() } catch (e) { } // reset the stream data to empty ram
         testStream(i, 0)
       }.bind(this)
       xhr[i].onerror = function () {
         // error
+		tlog("dl stream failed "+i);
         if (settings.xhr_ignoreErrors === 0) failed=true //abort
         try { xhr[i].abort() } catch (e) { }
         delete (xhr[i])
@@ -191,6 +210,7 @@ function dlTest (done) {
   }
   // every 200ms, update dlStatus
   interval = setInterval(function () {
+	tlog("DL: "+dlStatus+(graceTimeDone?"":" (in grace time)"))
     var t = new Date().getTime() - startT
     if (t < 200) return
     if (!graceTimeDone){
@@ -208,6 +228,7 @@ function dlTest (done) {
         if (failed || isNaN(dlStatus)) dlStatus = 'Fail'
         clearRequests()
         clearInterval(interval)
+		tlog("dlTest finished "+dlStatus)
         done()
       }
     }
@@ -227,6 +248,7 @@ reqsmall.push(r)
 reqsmall = new Blob(reqsmall)
 var ulCalled = false // used to prevent multiple accidental calls to ulTest
 function ulTest (done) {
+  tlog("ulTest");
   if (ulCalled) return; else ulCalled = true // ulTest already called?
   if (settings.url_ul == "-1") {done(); return}
   var totLoaded = 0.0, // total number of transmitted bytes
@@ -238,24 +260,29 @@ function ulTest (done) {
   var testStream = function (i, delay) {
     setTimeout(function () {
       if (testStatus !== 3) return // delayed stream ended up starting after the end of the upload test
+	  tlog("ul test stream started "+i+" "+delay)
       var prevLoaded = 0 // number of bytes transmitted last time onprogress was called
       var x = new XMLHttpRequest()
       xhr[i] = x
       var ie11workaround
-      try {
-        xhr[i].upload.onprogress
-        ie11workaround = false
-      } catch (e) {
-        ie11workaround = true
+      if (settings.forceIE11Workaround) ie11workaround = true; else {
+        try {
+          xhr[i].upload.onprogress
+          ie11workaround = false
+        } catch (e) {
+          ie11workaround = true
+        }
       }
       if (ie11workaround) {
         // IE11 workarond: xhr.upload does not work properly, therefore we send a bunch of small 256k requests and use the onload event as progress. This is not precise, especially on fast connections
         xhr[i].onload = function () {
+		  tlog("ul stream progress event (ie11wa)")
           totLoaded += 262144
           testStream(i, 0)
         }
         xhr[i].onerror = function () {
           // error, abort
+		  tlog("ul stream failed (ie11wa)")
           if (settings.xhr_ignoreErrors === 0) failed = true //abort
           try { xhr[i].abort() } catch (e) { }
           delete (xhr[i])
@@ -267,6 +294,7 @@ function ulTest (done) {
       } else {
         // REGULAR version, no workaround
         xhr[i].upload.onprogress = function (event) {
+		  tlog("ul stream progress event "+i+" "+event.loaded);
           if (testStatus !== 3) { try { x.abort() } catch (e) { } } // just in case this XHR is still running after the upload test
           // progress event, add number of new loaded bytes to totLoaded
           var loadDiff = event.loaded <= 0 ? 0 : (event.loaded - prevLoaded)
@@ -276,9 +304,11 @@ function ulTest (done) {
         }.bind(this)
         xhr[i].upload.onload = function () {
           // this stream sent all the garbage data, start again
+		  tlog("ul stream finished "+i);
           testStream(i, 0)
         }.bind(this)
         xhr[i].upload.onerror = function () {
+		  tlog("ul stream failed "+i);
           if (settings.xhr_ignoreErrors === 0) failed=true //abort
           try { xhr[i].abort() } catch (e) { }
           delete (xhr[i])
@@ -297,6 +327,7 @@ function ulTest (done) {
   }
   // every 200ms, update ulStatus
   interval = setInterval(function () {
+	tlog("UL: "+ulStatus+(graceTimeDone?"":" (in grace time)"))
     var t = new Date().getTime() - startT
     if (t < 200) return
     if (!graceTimeDone){
@@ -314,6 +345,7 @@ function ulTest (done) {
         if (failed || isNaN(ulStatus)) ulStatus = 'Fail'
         clearRequests()
         clearInterval(interval)
+		tlog("ulTest finished "+ulStatus)
         done()
       }
     }
@@ -322,6 +354,7 @@ function ulTest (done) {
 // ping+jitter test, function done is called when it's over
 var ptCalled = false // used to prevent multiple accidental calls to pingTest
 function pingTest (done) {
+  tlog("pingTest");
   if (ptCalled) return; else ptCalled = true // pingTest already called?
   if (settings.url_ping == "-1") {done(); return}
   var prevT = null // last time a pong was received
@@ -332,10 +365,12 @@ function pingTest (done) {
   xhr = []
   // ping function
   var doPing = function () {
+	tlog("ping");
     prevT = new Date().getTime()
     xhr[0] = new XMLHttpRequest()
     xhr[0].onload = function () {
       // pong
+	  tlog("pong");
       if (i === 0) {
         prevT = new Date().getTime() // first pong
       } else {
@@ -350,10 +385,12 @@ function pingTest (done) {
       pingStatus = ping.toFixed(2)
       jitterStatus = jitter.toFixed(2)
       i++
+	  tlog("PING: "+pingStatus+" JITTER: "+jitterStatus)
       if (i < settings.count_ping) doPing(); else done() // more pings to do?
     }.bind(this)
     xhr[0].onerror = function () {
       // a ping failed, cancel test
+	  tlog("ping failed");
       if (settings.xhr_ignoreErrors === 0) { //abort
         pingStatus = 'Fail'
         jitterStatus = 'Fail'
@@ -361,7 +398,6 @@ function pingTest (done) {
         done()
       }
       if (settings.xhr_ignoreErrors === 1) doPing() //retry ping
-
       if (settings.xhr_ignoreErrors === 2){ //ignore failed ping
         i++
         if (i < settings.count_ping) doPing(); else done() // more pings to do?
@@ -373,3 +409,26 @@ function pingTest (done) {
   }.bind(this)
   doPing() // start first ping
 }
+// telemetry
+function sendTelemetry(){
+  if (settings.telemetry_level < 1) return
+  xhr = new XMLHttpRequest();
+  xhr.onload = function () { console.log("TELEMETRY OL "+xhr.responseText) }
+  xhr.onerror = function () { console.log("TELEMETRY ERROR "+xhr) }
+  xhr.open('POST', settings.url_telemetry+"?r="+Math.random(), true);
+  try{
+    var fd = new FormData()
+    fd.append('dl', dlStatus)
+    fd.append('ul', ulStatus)
+    fd.append('ping', pingStatus)
+    fd.append('jitter', jitterStatus)
+    fd.append('log', settings.telemetry_level>1?log:"")
+    xhr.send(fd)
+  }catch(ex){
+    var postData = "dl="+encodeURIComponent(dlStatus)+"&ul="+encodeURIComponent(ulStatus)+"&ping="+encodeURIComponent(pingStatus)+"&jitter="+encodeURIComponent(jitterStatus)+"&log="+encodeURIComponent(settings.telemetry_level>1?log:"")
+    xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded")
+    xhr.send(postData)
+  }
+
+
+}
\ No newline at end of file
diff --git a/speedtest_worker.min.js b/speedtest_worker.min.js
index ae33a00ebe06942904a25ac715773a47a3ae16fe..f7a2a8cf4a687ffc58dd8c20d1f0f4f1aaafa3c9 100644
--- a/speedtest_worker.min.js
+++ b/speedtest_worker.min.js
@@ -1 +1 @@
-function url_sep(url){return url.match(/\?/)?"&":"?"}function clearRequests(){if(xhr){for(var i=0;i<xhr.length;i++){if(useFetchAPI)try{xhr[i].cancelRequested=!0}catch(e){}try{xhr[i].onprogress=null,xhr[i].onload=null,xhr[i].onerror=null}catch(e){}try{xhr[i].upload.onprogress=null,xhr[i].upload.onload=null,xhr[i].upload.onerror=null}catch(e){}try{xhr[i].abort()}catch(e){}try{delete xhr[i]}catch(e){}}xhr=null}}function getIp(done){"-1"!=settings.url_getIp?((xhr=new XMLHttpRequest).onload=function(){clientIp=xhr.responseText,done()},xhr.onerror=function(){done()},xhr.open("GET",settings.url_getIp+url_sep(settings.url_getIp)+"r="+Math.random(),!0),xhr.send()):done()}function dlTest(done){if(!dlCalled)if(dlCalled=!0,"-1"!=settings.url_dl){var totLoaded=0,startT=(new Date).getTime(),graceTimeDone=!1,failed=!1;xhr=[];for(var testStream=function(i,delay){setTimeout(function(){if(1===testStatus){var prevLoaded=0,x=new XMLHttpRequest;xhr[i]=x,xhr[i].onprogress=function(event){if(1!==testStatus)try{x.abort()}catch(e){}var loadDiff=event.loaded<=0?0:event.loaded-prevLoaded;isNaN(loadDiff)||!isFinite(loadDiff)||loadDiff<0||(totLoaded+=loadDiff,prevLoaded=event.loaded)}.bind(this),xhr[i].onload=function(){try{xhr[i].abort()}catch(e){}testStream(i,0)}.bind(this),xhr[i].onerror=function(){0===settings.xhr_ignoreErrors&&(failed=!0);try{xhr[i].abort()}catch(e){}delete xhr[i],1===settings.xhr_ignoreErrors&&testStream(i,100)}.bind(this);try{settings.xhr_dlUseBlob?xhr[i].responseType="blob":xhr[i].responseType="arraybuffer"}catch(e){}xhr[i].open("GET",settings.url_dl+url_sep(settings.url_dl)+"r="+Math.random()+"&ckSize="+settings.garbagePhp_chunkSize,!0),xhr[i].send()}}.bind(this),1+delay)}.bind(this),i=0;i<settings.xhr_dlMultistream;i++)testStream(i,100*i);interval=setInterval(function(){var t=(new Date).getTime()-startT;t<200||(graceTimeDone?(dlStatus=(8*(totLoaded/(t/1e3))*settings.overheadCompensationFactor/1048576).toFixed(2),(t/1e3>settings.time_dl&&dlStatus>0||failed)&&((failed||isNaN(dlStatus))&&(dlStatus="Fail"),clearRequests(),clearInterval(interval),done())):t>1e3*settings.time_dlGraceTime&&(totLoaded>0&&(startT=(new Date).getTime(),totLoaded=0),graceTimeDone=!0))}.bind(this),200)}else done()}function ulTest(done){if(!ulCalled)if(ulCalled=!0,"-1"!=settings.url_ul){var totLoaded=0,startT=(new Date).getTime(),graceTimeDone=!1,failed=!1;xhr=[];for(var testStream=function(i,delay){setTimeout(function(){if(3===testStatus){var prevLoaded=0,x=new XMLHttpRequest;xhr[i]=x;var ie11workaround;try{xhr[i].upload.onprogress,ie11workaround=!1}catch(e){ie11workaround=!0}ie11workaround?(xhr[i].onload=function(){totLoaded+=262144,testStream(i,0)},xhr[i].onerror=function(){0===settings.xhr_ignoreErrors&&(failed=!0);try{xhr[i].abort()}catch(e){}delete xhr[i],1===settings.xhr_ignoreErrors&&testStream(i,100)},xhr[i].open("POST",settings.url_ul+url_sep(settings.url_ul)+"r="+Math.random(),!0),xhr[i].setRequestHeader("Content-Encoding","identity"),xhr[i].send(reqsmall)):(xhr[i].upload.onprogress=function(event){if(3!==testStatus)try{x.abort()}catch(e){}var loadDiff=event.loaded<=0?0:event.loaded-prevLoaded;isNaN(loadDiff)||!isFinite(loadDiff)||loadDiff<0||(totLoaded+=loadDiff,prevLoaded=event.loaded)}.bind(this),xhr[i].upload.onload=function(){testStream(i,0)}.bind(this),xhr[i].upload.onerror=function(){0===settings.xhr_ignoreErrors&&(failed=!0);try{xhr[i].abort()}catch(e){}delete xhr[i],1===settings.xhr_ignoreErrors&&testStream(i,100)}.bind(this),xhr[i].open("POST",settings.url_ul+url_sep(settings.url_ul)+"r="+Math.random(),!0),xhr[i].setRequestHeader("Content-Encoding","identity"),xhr[i].send(req))}}.bind(this),1)}.bind(this),i=0;i<settings.xhr_ulMultistream;i++)testStream(i,100*i);interval=setInterval(function(){var t=(new Date).getTime()-startT;t<200||(graceTimeDone?(ulStatus=(8*(totLoaded/(t/1e3))*settings.overheadCompensationFactor/1048576).toFixed(2),(t/1e3>settings.time_ul&&ulStatus>0||failed)&&((failed||isNaN(ulStatus))&&(ulStatus="Fail"),clearRequests(),clearInterval(interval),done())):t>1e3*settings.time_ulGraceTime&&(totLoaded>0&&(startT=(new Date).getTime(),totLoaded=0),graceTimeDone=!0))}.bind(this),200)}else done()}function pingTest(done){if(!ptCalled)if(ptCalled=!0,"-1"!=settings.url_ping){var prevT=null,ping=0,jitter=0,i=0,prevInstspd=0;xhr=[];var doPing=function(){prevT=(new Date).getTime(),xhr[0]=new XMLHttpRequest,xhr[0].onload=function(){if(0===i)prevT=(new Date).getTime();else{var instspd=(new Date).getTime()-prevT,instjitter=Math.abs(instspd-prevInstspd);1===i?ping=instspd:(ping=.9*ping+.1*instspd,jitter=instjitter>jitter?.2*jitter+.8*instjitter:.9*jitter+.1*instjitter),prevInstspd=instspd}pingStatus=ping.toFixed(2),jitterStatus=jitter.toFixed(2),++i<settings.count_ping?doPing():done()}.bind(this),xhr[0].onerror=function(){0===settings.xhr_ignoreErrors&&(pingStatus="Fail",jitterStatus="Fail",clearRequests(),done()),1===settings.xhr_ignoreErrors&&doPing(),2===settings.xhr_ignoreErrors&&(++i<settings.count_ping?doPing():done())}.bind(this),xhr[0].open("GET",settings.url_ping+url_sep(settings.url_ping)+"r="+Math.random(),!0),xhr[0].send()}.bind(this);doPing()}else done()}var testStatus=0,dlStatus="",ulStatus="",pingStatus="",jitterStatus="",clientIp="",settings={time_ul:15,time_dl:15,time_ulGraceTime:3,time_dlGraceTime:1.5,count_ping:35,url_dl:"garbage.php",url_ul:"empty.php",url_ping:"empty.php",url_getIp:"getIP.php",xhr_dlMultistream:10,xhr_ulMultistream:3,xhr_ignoreErrors:1,xhr_dlUseBlob:!1,garbagePhp_chunkSize:20,enable_quirks:!0,overheadCompensationFactor:1048576/925e3},xhr=null,interval=null,useFetchAPI=!1;this.addEventListener("message",function(e){var params=e.data.split(" ");if("status"===params[0]&&postMessage(testStatus+";"+dlStatus+";"+ulStatus+";"+pingStatus+";"+clientIp+";"+jitterStatus),"start"===params[0]&&0===testStatus){testStatus=1;try{var s={};try{var ss=e.data.substring(5);ss&&(s=JSON.parse(ss))}catch(e){console.warn("Error parsing custom settings JSON. Please check your syntax")}if("undefined"!=typeof s.url_dl&&(settings.url_dl=s.url_dl),"undefined"!=typeof s.url_ul&&(settings.url_ul=s.url_ul),"undefined"!=typeof s.url_ping&&(settings.url_ping=s.url_ping),"undefined"!=typeof s.url_getIp&&(settings.url_getIp=s.url_getIp),"undefined"!=typeof s.time_dl&&(settings.time_dl=s.time_dl),"undefined"!=typeof s.time_ul&&(settings.time_ul=s.time_ul),"undefined"!=typeof s.enable_quirks&&(settings.enable_quirks=s.enable_quirks),settings.enable_quirks){var ua=navigator.userAgent;/Firefox.(\d+\.\d+)/i.test(ua)&&(settings.xhr_ulMultistream=1),/Edge.(\d+\.\d+)/i.test(ua)&&(settings.xhr_dlMultistream=3),/Chrome.(\d+)/i.test(ua)&&self.fetch&&(settings.xhr_dlMultistream=5)}"undefined"!=typeof s.count_ping&&(settings.count_ping=s.count_ping),"undefined"!=typeof s.xhr_dlMultistream&&(settings.xhr_dlMultistream=s.xhr_dlMultistream),"undefined"!=typeof s.xhr_ulMultistream&&(settings.xhr_ulMultistream=s.xhr_ulMultistream),"undefined"!=typeof s.xhr_ignoreErrors&&(settings.xhr_ignoreErrors=s.xhr_ignoreErrors),"undefined"!=typeof s.xhr_dlUseBlob&&(settings.xhr_dlUseBlob=s.xhr_dlUseBlob),"undefined"!=typeof s.garbagePhp_chunkSize&&(settings.garbagePhp_chunkSize=s.garbagePhp_chunkSize),"undefined"!=typeof s.time_dlGraceTime&&(settings.time_dlGraceTime=s.time_dlGraceTime),"undefined"!=typeof s.time_ulGraceTime&&(settings.time_ulGraceTime=s.time_ulGraceTime),"undefined"!=typeof s.overheadCompensationFactor&&(settings.overheadCompensationFactor=s.overheadCompensationFactor)}catch(e){console.warn("Possible error in custom test settings. Some settings may not be applied. Exception: "+e)}console.log(settings),getIp(function(){dlTest(function(){testStatus=2,pingTest(function(){testStatus=3,ulTest(function(){testStatus=4})})})})}"abort"===params[0]&&(clearRequests(),interval&&clearInterval(interval),testStatus=5,dlStatus="",ulStatus="",pingStatus="",jitterStatus="")});var dlCalled=!1,r=new ArrayBuffer(1048576);try{r=new Float32Array(r);for(var i=0;i<r.length;i++)r[i]=Math.random()}catch(e){}for(var req=[],reqsmall=[],i=0;i<20;i++)req.push(r);req=new Blob(req),r=new ArrayBuffer(262144);try{r=new Float32Array(r);for(var i=0;i<r.length;i++)r[i]=Math.random()}catch(e){}reqsmall.push(r),reqsmall=new Blob(reqsmall);var ulCalled=!1,ptCalled=!1;
+function tlog(s){log+=Date.now()+": "+s+"\n"}function twarn(s){log+=Date.now()+" WARN: "+s+"\n",console.warn(s)}function url_sep(url){return url.match(/\?/)?"&":"?"}function clearRequests(){if(tlog("stopping pending XHRs"),xhr){for(var i=0;i<xhr.length;i++){if(useFetchAPI)try{xhr[i].cancelRequested=!0}catch(e){}try{xhr[i].onprogress=null,xhr[i].onload=null,xhr[i].onerror=null}catch(e){}try{xhr[i].upload.onprogress=null,xhr[i].upload.onload=null,xhr[i].upload.onerror=null}catch(e){}try{xhr[i].abort()}catch(e){}try{delete xhr[i]}catch(e){}}xhr=null}}function getIp(done){return tlog("getIp"),"-1"==settings.url_getIp?void done():(xhr=new XMLHttpRequest,xhr.onload=function(){tlog("IP: "+xhr.responseText),clientIp=xhr.responseText,done()},xhr.onerror=function(){tlog("getIp failed"),done()},xhr.open("GET",settings.url_getIp+url_sep(settings.url_getIp)+"r="+Math.random(),!0),void xhr.send())}function dlTest(done){if(tlog("dlTest"),!dlCalled){if(dlCalled=!0,"-1"==settings.url_dl)return void done();var totLoaded=0,startT=(new Date).getTime(),graceTimeDone=!1,failed=!1;xhr=[];for(var testStream=function(i,delay){setTimeout(function(){if(1===testStatus){tlog("dl test stream started "+i+" "+delay);var prevLoaded=0,x=new XMLHttpRequest;xhr[i]=x,xhr[i].onprogress=function(event){if(tlog("dl stream progress event "+i+" "+event.loaded),1!==testStatus)try{x.abort()}catch(e){}var loadDiff=event.loaded<=0?0:event.loaded-prevLoaded;isNaN(loadDiff)||!isFinite(loadDiff)||0>loadDiff||(totLoaded+=loadDiff,prevLoaded=event.loaded)}.bind(this),xhr[i].onload=function(){tlog("dl stream finished "+i);try{xhr[i].abort()}catch(e){}testStream(i,0)}.bind(this),xhr[i].onerror=function(){tlog("dl stream failed "+i),0===settings.xhr_ignoreErrors&&(failed=!0);try{xhr[i].abort()}catch(e){}delete xhr[i],1===settings.xhr_ignoreErrors&&testStream(i,100)}.bind(this);try{settings.xhr_dlUseBlob?xhr[i].responseType="blob":xhr[i].responseType="arraybuffer"}catch(e){}xhr[i].open("GET",settings.url_dl+url_sep(settings.url_dl)+"r="+Math.random()+"&ckSize="+settings.garbagePhp_chunkSize,!0),xhr[i].send()}}.bind(this),1+delay)}.bind(this),i=0;i<settings.xhr_dlMultistream;i++)testStream(i,100*i);interval=setInterval(function(){tlog("DL: "+dlStatus+(graceTimeDone?"":" (in grace time)"));var t=(new Date).getTime()-startT;if(!(200>t))if(graceTimeDone){var speed=totLoaded/(t/1e3);dlStatus=(8*speed*settings.overheadCompensationFactor/1048576).toFixed(2),(t/1e3>settings.time_dl&&dlStatus>0||failed)&&((failed||isNaN(dlStatus))&&(dlStatus="Fail"),clearRequests(),clearInterval(interval),tlog("dlTest finished "+dlStatus),done())}else t>1e3*settings.time_dlGraceTime&&(totLoaded>0&&(startT=(new Date).getTime(),totLoaded=0),graceTimeDone=!0)}.bind(this),200)}}function ulTest(done){if(tlog("ulTest"),!ulCalled){if(ulCalled=!0,"-1"==settings.url_ul)return void done();var totLoaded=0,startT=(new Date).getTime(),graceTimeDone=!1,failed=!1;xhr=[];for(var testStream=function(i,delay){setTimeout(function(){if(3===testStatus){tlog("ul test stream started "+i+" "+delay);var prevLoaded=0,x=new XMLHttpRequest;xhr[i]=x;var ie11workaround;if(settings.forceIE11Workaround)ie11workaround=!0;else try{xhr[i].upload.onprogress,ie11workaround=!1}catch(e){ie11workaround=!0}ie11workaround?(xhr[i].onload=function(){tlog("ul stream progress event (ie11wa)"),totLoaded+=262144,testStream(i,0)},xhr[i].onerror=function(){tlog("ul stream failed (ie11wa)"),0===settings.xhr_ignoreErrors&&(failed=!0);try{xhr[i].abort()}catch(e){}delete xhr[i],1===settings.xhr_ignoreErrors&&testStream(i,100)},xhr[i].open("POST",settings.url_ul+url_sep(settings.url_ul)+"r="+Math.random(),!0),xhr[i].setRequestHeader("Content-Encoding","identity"),xhr[i].send(reqsmall)):(xhr[i].upload.onprogress=function(event){if(tlog("ul stream progress event "+i+" "+event.loaded),3!==testStatus)try{x.abort()}catch(e){}var loadDiff=event.loaded<=0?0:event.loaded-prevLoaded;isNaN(loadDiff)||!isFinite(loadDiff)||0>loadDiff||(totLoaded+=loadDiff,prevLoaded=event.loaded)}.bind(this),xhr[i].upload.onload=function(){tlog("ul stream finished "+i),testStream(i,0)}.bind(this),xhr[i].upload.onerror=function(){tlog("ul stream failed "+i),0===settings.xhr_ignoreErrors&&(failed=!0);try{xhr[i].abort()}catch(e){}delete xhr[i],1===settings.xhr_ignoreErrors&&testStream(i,100)}.bind(this),xhr[i].open("POST",settings.url_ul+url_sep(settings.url_ul)+"r="+Math.random(),!0),xhr[i].setRequestHeader("Content-Encoding","identity"),xhr[i].send(req))}}.bind(this),1)}.bind(this),i=0;i<settings.xhr_ulMultistream;i++)testStream(i,100*i);interval=setInterval(function(){tlog("UL: "+ulStatus+(graceTimeDone?"":" (in grace time)"));var t=(new Date).getTime()-startT;if(!(200>t))if(graceTimeDone){var speed=totLoaded/(t/1e3);ulStatus=(8*speed*settings.overheadCompensationFactor/1048576).toFixed(2),(t/1e3>settings.time_ul&&ulStatus>0||failed)&&((failed||isNaN(ulStatus))&&(ulStatus="Fail"),clearRequests(),clearInterval(interval),tlog("ulTest finished "+ulStatus),done())}else t>1e3*settings.time_ulGraceTime&&(totLoaded>0&&(startT=(new Date).getTime(),totLoaded=0),graceTimeDone=!0)}.bind(this),200)}}function pingTest(done){if(tlog("pingTest"),!ptCalled){if(ptCalled=!0,"-1"==settings.url_ping)return void done();var prevT=null,ping=0,jitter=0,i=0,prevInstspd=0;xhr=[];var doPing=function(){tlog("ping"),prevT=(new Date).getTime(),xhr[0]=new XMLHttpRequest,xhr[0].onload=function(){if(tlog("pong"),0===i)prevT=(new Date).getTime();else{var instspd=(new Date).getTime()-prevT,instjitter=Math.abs(instspd-prevInstspd);1===i?ping=instspd:(ping=.9*ping+.1*instspd,jitter=instjitter>jitter?.2*jitter+.8*instjitter:.9*jitter+.1*instjitter),prevInstspd=instspd}pingStatus=ping.toFixed(2),jitterStatus=jitter.toFixed(2),i++,tlog("PING: "+pingStatus+" JITTER: "+jitterStatus),i<settings.count_ping?doPing():done()}.bind(this),xhr[0].onerror=function(){tlog("ping failed"),0===settings.xhr_ignoreErrors&&(pingStatus="Fail",jitterStatus="Fail",clearRequests(),done()),1===settings.xhr_ignoreErrors&&doPing(),2===settings.xhr_ignoreErrors&&(i++,i<settings.count_ping?doPing():done())}.bind(this),xhr[0].open("GET",settings.url_ping+url_sep(settings.url_ping)+"r="+Math.random(),!0),xhr[0].send()}.bind(this);doPing()}}function sendTelemetry(){if(!(settings.telemetry_level<1)){xhr=new XMLHttpRequest,xhr.onload=function(){console.log("TELEMETRY OL "+xhr.responseText)},xhr.onerror=function(){console.log("TELEMETRY ERROR "+xhr)},xhr.open("POST",settings.url_telemetry+"?r="+Math.random(),!0);try{var fd=new FormData;fd.append("dl",dlStatus),fd.append("ul",ulStatus),fd.append("ping",pingStatus),fd.append("jitter",jitterStatus),fd.append("log",settings.telemetry_level>1?log:""),xhr.send(fd)}catch(ex){var postData="dl="+encodeURIComponent(dlStatus)+"&ul="+encodeURIComponent(ulStatus)+"&ping="+encodeURIComponent(pingStatus)+"&jitter="+encodeURIComponent(jitterStatus)+"&log="+encodeURIComponent(settings.telemetry_level>1?log:"");xhr.setRequestHeader("Content-Type","application/x-www-form-urlencoded"),xhr.send(postData)}}}var testStatus=0,dlStatus="",ulStatus="",pingStatus="",jitterStatus="",clientIp="",log="",settings={time_ul:15,time_dl:15,time_ulGraceTime:3,time_dlGraceTime:1.5,count_ping:35,url_dl:"garbage.php",url_ul:"empty.php",url_ping:"empty.php",url_getIp:"getIP.php",xhr_dlMultistream:10,xhr_ulMultistream:3,xhr_ignoreErrors:1,xhr_dlUseBlob:!1,garbagePhp_chunkSize:20,enable_quirks:!0,overheadCompensationFactor:1048576/925e3,telemetry_level:0,url_telemetry:"telemetry.php"},xhr=null,interval=null,useFetchAPI=!1;this.addEventListener("message",function(e){var params=e.data.split(" ");if("status"===params[0]&&postMessage(testStatus+";"+dlStatus+";"+ulStatus+";"+pingStatus+";"+clientIp+";"+jitterStatus),"start"===params[0]&&0===testStatus){testStatus=1;try{var s={};try{var ss=e.data.substring(5);ss&&(s=JSON.parse(ss))}catch(e){console.warn("Error parsing custom settings JSON. Please check your syntax")}if("undefined"!=typeof s.url_dl&&(settings.url_dl=s.url_dl),"undefined"!=typeof s.url_ul&&(settings.url_ul=s.url_ul),"undefined"!=typeof s.url_ping&&(settings.url_ping=s.url_ping),"undefined"!=typeof s.url_getIp&&(settings.url_getIp=s.url_getIp),"undefined"!=typeof s.time_dl&&(settings.time_dl=s.time_dl),"undefined"!=typeof s.time_ul&&(settings.time_ul=s.time_ul),"undefined"!=typeof s.enable_quirks&&(settings.enable_quirks=s.enable_quirks),settings.enable_quirks){var ua=navigator.userAgent;/Firefox.(\d+\.\d+)/i.test(ua)&&(settings.xhr_ulMultistream=1),/Edge.(\d+\.\d+)/i.test(ua)&&(settings.xhr_dlMultistream=3,settings.forceIE11Workaround=!0),/Chrome.(\d+)/i.test(ua)&&self.fetch&&(settings.xhr_dlMultistream=5)}"undefined"!=typeof s.count_ping&&(settings.count_ping=s.count_ping),"undefined"!=typeof s.xhr_dlMultistream&&(settings.xhr_dlMultistream=s.xhr_dlMultistream),"undefined"!=typeof s.xhr_ulMultistream&&(settings.xhr_ulMultistream=s.xhr_ulMultistream),"undefined"!=typeof s.xhr_ignoreErrors&&(settings.xhr_ignoreErrors=s.xhr_ignoreErrors),"undefined"!=typeof s.xhr_dlUseBlob&&(settings.xhr_dlUseBlob=s.xhr_dlUseBlob),"undefined"!=typeof s.garbagePhp_chunkSize&&(settings.garbagePhp_chunkSize=s.garbagePhp_chunkSize),"undefined"!=typeof s.time_dlGraceTime&&(settings.time_dlGraceTime=s.time_dlGraceTime),"undefined"!=typeof s.time_ulGraceTime&&(settings.time_ulGraceTime=s.time_ulGraceTime),"undefined"!=typeof s.overheadCompensationFactor&&(settings.overheadCompensationFactor=s.overheadCompensationFactor),"undefined"!=typeof s.telemetry_level&&(settings.telemetry_level="basic"===s.telemetry_level?1:"full"===s.telemetry_level?2:0),"undefined"!=typeof s.url_telemetry&&(settings.url_telemetry=s.url_telemetry)}catch(e){twarn("Possible error in custom test settings. Some settings may not be applied. Exception: "+e)}tlog(JSON.stringify(settings)),getIp(function(){dlTest(function(){testStatus=2,pingTest(function(){testStatus=3,ulTest(function(){testStatus=4,sendTelemetry()})})})})}"abort"===params[0]&&(tlog("manually aborted"),clearRequests(),interval&&clearInterval(interval),settings.telemetry_level>1&&sendTelemetry(),testStatus=5,dlStatus="",ulStatus="",pingStatus="",jitterStatus="")});var dlCalled=!1,r=new ArrayBuffer(1048576);try{r=new Float32Array(r);for(var i=0;i<r.length;i++)r[i]=Math.random()}catch(e){}for(var req=[],reqsmall=[],i=0;20>i;i++)req.push(r);req=new Blob(req),r=new ArrayBuffer(262144);try{r=new Float32Array(r);for(var i=0;i<r.length;i++)r[i]=Math.random()}catch(e){}reqsmall.push(r),reqsmall=new Blob(reqsmall);var ulCalled=!1,ptCalled=!1;
diff --git a/telemetry.php b/telemetry.php
new file mode 100644
index 0000000000000000000000000000000000000000..14d446359ae5403d00b135ec9783ff318d04a144
--- /dev/null
+++ b/telemetry.php
@@ -0,0 +1,23 @@
+<?php
+	$MySql_username="USERNAME";
+	$MySql_password="PASSWORD";
+	$MySql_hostname="DB_HOSTNAME";
+	$MySql_databasename="DB_NAME";
+
+$ip=($_SERVER['REMOTE_ADDR']);
+$ua=($_SERVER['HTTP_USER_AGENT']);
+$lang=($_SERVER['HTTP_ACCEPT_LANGUAGE']);
+$dl=($_POST["dl"]);
+$ul=($_POST["ul"]);
+$ping=($_POST["ping"]);
+$jitter=($_POST["jitter"]);
+$log=($_POST["log"]);
+
+$conn = new mysqli($MySql_hostname, $MySql_username, $MySql_password, $MySql_databasename) or die("1");
+$stmt = $conn->prepare("INSERT INTO speedtest_users (ip,ua,lang,dl,ul,ping,jitter,log) VALUES (?,?,?,?,?,?,?,?)") or die("2");
+$stmt->bind_param("ssssssss",$ip,$ua,$lang,$dl,$ul,$ping,$jitter,$log) or die("3");
+$stmt->execute() or die("4");
+$stmt->close() or die("5");
+$conn->close() or die("6");
+
+?>
\ No newline at end of file
diff --git a/telemetry.sql b/telemetry.sql
new file mode 100644
index 0000000000000000000000000000000000000000..160b4fde3c5a514f70cdf23230b64b06246b179b
--- /dev/null
+++ b/telemetry.sql
@@ -0,0 +1,66 @@
+-- phpMyAdmin SQL Dump
+-- version 4.7.0
+-- https://www.phpmyadmin.net/
+--
+-- Host: 127.0.0.1
+-- Generation Time: Aug 24, 2017 at 02:16 PM
+-- Server version: 10.1.25-MariaDB
+-- PHP Version: 7.1.7
+
+SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
+SET AUTOCOMMIT = 0;
+START TRANSACTION;
+SET time_zone = "+00:00";
+
+
+/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
+/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
+/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
+/*!40101 SET NAMES utf8mb4 */;
+
+--
+-- Database: `speedtest_telemetry`
+--
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `speedtest_users`
+--
+
+CREATE TABLE `speedtest_users` (
+  `id` int(11) NOT NULL,
+  `timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  `ip` text NOT NULL,
+  `ua` text NOT NULL,
+  `lang` text NOT NULL,
+  `dl` text,
+  `ul` text,
+  `ping` text,
+  `jitter` text,
+  `log` longtext
+) ENGINE=InnoDB DEFAULT CHARSET=latin1;
+
+--
+-- Indexes for dumped tables
+--
+
+--
+-- Indexes for table `speedtest_users`
+--
+ALTER TABLE `speedtest_users`
+  ADD PRIMARY KEY (`id`);
+
+--
+-- AUTO_INCREMENT for dumped tables
+--
+
+--
+-- AUTO_INCREMENT for table `speedtest_users`
+--
+ALTER TABLE `speedtest_users`
+  MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;COMMIT;
+
+/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
+/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
+/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;