diff --git a/README.md b/README.md
index 3c6d906..e055d20 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@ It is based on Google's [Blockly](https://developers.google.com/blockly/) and Ca
## Development
-This is an Ionic 2 / Angular 5 application using Cordova Plugins for mobile-native functionality.
+This is an Ionic 3 / Angular 5 application using Cordova Plugins for mobile-native functionality.
### dev env setup
For a basic web version, only Node.js 8+ is required.
diff --git a/config.xml b/config.xml
index 921ca42..b409d72 100644
--- a/config.xml
+++ b/config.xml
@@ -86,4 +86,5 @@
+
diff --git a/package-lock.json b/package-lock.json
index d8a78ff..f54d8e6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
- "name": "openSenseApp",
- "version": "0.0.1",
+ "name": "blockly-sensebox",
+ "version": "1.0.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -91,6 +91,14 @@
"tslib": "^1.7.1"
}
},
+ "@ionic-native/app-version": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/@ionic-native/app-version/-/app-version-5.1.0.tgz",
+ "integrity": "sha512-u+j319sZBBGjf0MjeMhxKpT+iwL8R6bq4MBAbQOV+x/pyGuqed0RvoEZ5TO/XEpBLYrizkiKGffaNqlxHm1W+A==",
+ "requires": {
+ "@types/cordova": "^0.0.34"
+ }
+ },
"@ionic-native/core": {
"version": "4.15.0",
"resolved": "https://registry.npmjs.org/@ionic-native/core/-/core-4.15.0.tgz",
@@ -1060,6 +1068,11 @@
"resolved": "https://registry.npmjs.org/@ngx-translate/http-loader/-/http-loader-2.0.1.tgz",
"integrity": "sha1-qmd4jmS/qGUmkad7Ais7QDEgkRM="
},
+ "@types/cordova": {
+ "version": "0.0.34",
+ "resolved": "https://registry.npmjs.org/@types/cordova/-/cordova-0.0.34.tgz",
+ "integrity": "sha1-6nrd907Ow9dimCegw54smt3HPQQ="
+ },
"abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
@@ -2830,6 +2843,11 @@
}
}
},
+ "cordova-plugin-app-version": {
+ "version": "0.1.9",
+ "resolved": "https://registry.npmjs.org/cordova-plugin-app-version/-/cordova-plugin-app-version-0.1.9.tgz",
+ "integrity": "sha1-nbBgeGMzenEEiTAuX1CpBPFEm9s="
+ },
"cordova-plugin-device": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/cordova-plugin-device/-/cordova-plugin-device-2.0.2.tgz",
@@ -3663,8 +3681,7 @@
},
"ansi-regex": {
"version": "2.1.1",
- "bundled": true,
- "optional": true
+ "bundled": true
},
"aproba": {
"version": "1.2.0",
@@ -4029,8 +4046,7 @@
},
"safe-buffer": {
"version": "5.1.1",
- "bundled": true,
- "optional": true
+ "bundled": true
},
"safer-buffer": {
"version": "2.1.2",
@@ -4078,7 +4094,6 @@
"strip-ansi": {
"version": "3.0.1",
"bundled": true,
- "optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@@ -4117,13 +4132,11 @@
},
"wrappy": {
"version": "1.0.2",
- "bundled": true,
- "optional": true
+ "bundled": true
},
"yallist": {
"version": "3.0.2",
- "bundled": true,
- "optional": true
+ "bundled": true
}
}
},
@@ -8003,6 +8016,7 @@
"version": "1.1.11",
"bundled": true,
"dev": true,
+ "optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
diff --git a/package.json b/package.json
index a4259ef..f940d6c 100644
--- a/package.json
+++ b/package.json
@@ -25,6 +25,7 @@
"@angular/http": "5.2.11",
"@angular/platform-browser": "5.2.11",
"@angular/platform-browser-dynamic": "5.2.11",
+ "@ionic-native/app-version": "^5.1.0",
"@ionic-native/core": "~4.15.0",
"@ionic-native/network": "^4.17.0",
"@ionic-native/splash-screen": "~4.15.0",
@@ -34,6 +35,7 @@
"@ngx-translate/http-loader": "^2.0.1",
"cordova-android": "7.1.1",
"cordova-browser": "5.0.4",
+ "cordova-plugin-app-version": "0.1.9",
"cordova-plugin-device": "^2.0.2",
"cordova-plugin-ionic-keyboard": "^2.1.3",
"cordova-plugin-ionic-webview": "^2.3.3",
@@ -66,11 +68,12 @@
},
"cordova-plugin-ionic-keyboard": {},
"wifiwizard2": {},
- "cordova-plugin-network-information": {}
+ "cordova-plugin-network-information": {},
+ "cordova-plugin-app-version": {}
},
"platforms": [
"android",
"browser"
]
}
-}
\ No newline at end of file
+}
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index 05b61c7..bd1b7b0 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -10,6 +10,8 @@ import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { openSenseApp } from './app.component';
import { OtaWizardPageModule } from '../pages/ota-wizard/ota-wizard.module';
import { BlocklyPageModule } from '../pages/blockly/blockly.module';
+import { LoggingProvider } from '../providers/logging/logging';
+import { AppVersion } from '@ionic-native/app-version/ngx';
import { StorageProvider } from '../providers/storage/storage';
// For AoT compilation (production builds) we need to have a factory for the loader of translation files.
@@ -45,6 +47,8 @@ export function createTranslateLoader(http: HttpClient) {
StatusBar,
SplashScreen,
{provide: ErrorHandler, useClass: IonicErrorHandler},
+ AppVersion,
+ LoggingProvider,
StorageProvider,
]
})
diff --git a/src/constants.ts b/src/constants.ts
index f9f861d..330d72c 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -1,4 +1,11 @@
+import { LogLevel, LogOptions } from "./providers/logging/logging";
+
export const COLORS = {
PRIMARY: '#4EAF47', // sensebox green
}
export const DEFAULT_LANG = 'en'
+export const LOG_OPTIONS: LogOptions = {
+ local: LogLevel.INFO,
+ remote: LogLevel.WARN,
+ endpoint: 'https://logs.snsbx.nroo.de/log',
+}
diff --git a/src/providers/logging/logging.ts b/src/providers/logging/logging.ts
new file mode 100644
index 0000000..6bc3046
--- /dev/null
+++ b/src/providers/logging/logging.ts
@@ -0,0 +1,125 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import { AppVersion } from '@ionic-native/app-version/ngx';
+import { Platform } from 'ionic-angular';
+
+import { LOG_OPTIONS } from '../../constants';
+import { StorageProvider, SETTINGS } from '../storage/storage';
+import { TranslateService } from '@ngx-translate/core';
+
+// these types must be defined here to avoid a circular dependency with LoggingProvider
+export interface LogOptions {
+ local: boolean | LogLevel,
+ remote: boolean | LogLevel,
+ endpoint: string,
+}
+export enum LogLevel {
+ DEBUG = 0,
+ INFO = 1,
+ WARN = 2,
+ ERROR = 3,
+}
+
+@Injectable()
+export class LoggingProvider {
+ private opts: LogOptions = LOG_OPTIONS
+ private defaultFields: any = {}
+
+ constructor(
+ private http: HttpClient,
+ private plt: Platform,
+ private version: AppVersion,
+ private storage: StorageProvider,
+ private translate: TranslateService,
+ ) {
+ if ((window).cordova) {
+ this.version.getPackageName()
+ .then(name => this.defaultFields.app = name)
+ this.version.getVersionNumber()
+ .then(version => this.defaultFields.appVersion = version)
+ }
+ this.defaultFields.platform = this.plt.platforms().join(' ')
+ this.defaultFields.platformVersion = this.plt.version().str
+ this.defaultFields.lang = translate.currentLang
+ }
+
+ createChild (component: string, defaultFields: object = {}) {
+ const child = new LoggingProvider(this.http, this.plt, this.version, this.storage, this.translate)
+ Object.assign(child.defaultFields, defaultFields, { component })
+ return child
+ }
+
+ debug (...data) { return this.log(LogLevel.DEBUG, ...data) }
+ info (...data) { return this.log(LogLevel.INFO, ...data) }
+ warn (...data) { return this.log(LogLevel.WARN, ...data) }
+ error (...data) { return this.log(LogLevel.ERROR, ...data) }
+
+ private log (level: LogLevel, ...fields: (string | object)[]): LogMessage {
+ const msg = this.buildLogMessage(level, ...fields)
+
+ if (this.opts.local !== false && level >= this.opts.local) {
+ this.getLocalLogFunc(msg.level)(msg.time, msg.msg, msg)
+ }
+
+ if (this.opts.remote !== false && level >= this.opts.remote) {
+ if (this.storage.get(SETTINGS).logOptin) {
+ // fire & forget, no async handling as logging should not have impact on application flow
+ this.http.post(this.opts.endpoint, msg, { responseType: 'text' })
+ .toPromise()
+ .catch(console.error)
+ }
+ }
+
+ return msg
+ }
+
+ private buildLogMessage (level: LogLevel, ...fields: (string | object)[]): LogMessage {
+ const logentry = { } as LogMessage
+ let msg = ''
+
+ for (const param of fields) {
+ if (typeof param === 'object')
+ Object.assign(logentry, param)
+ else
+ msg = msg ? `${msg} ${param}` : param
+ }
+
+ if (msg)
+ logentry.msg = msg
+
+ Object.assign(logentry, this.defaultFields, {
+ time: Date.now(),
+ level,
+ })
+
+ return logentry
+ }
+
+ private getLocalLogFunc (level: LogLevel): (...params: any[]) => any {
+ switch (level) {
+ case LogLevel.DEBUG:
+ case LogLevel.INFO:
+ return console.log
+
+ case LogLevel.WARN:
+ return console.warn
+
+ case LogLevel.ERROR:
+ default:
+ return console.error
+ }
+ }
+}
+
+interface LogMessage {
+ level: LogLevel,
+ time: Date,
+ app: string,
+ appVersion: string,
+ platform: string,
+ platformVersion: string,
+
+ component?: string,
+ msg?: string,
+ [k: string]: any,
+}
diff --git a/tools/logserver.js b/tools/logserver.js
new file mode 100644
index 0000000..f575276
--- /dev/null
+++ b/tools/logserver.js
@@ -0,0 +1,65 @@
+'use strict';
+
+const http = require('http')
+const fs = require('fs')
+
+const port = process.argv[2] || 4444
+const logfile = process.argv[3] || '/tmp/logs.json'
+
+const resHeaders = {
+ 'Access-Control-Allow-Origin': '*',
+ 'Access-Control-Allow-Headers': 'content-type',
+ 'Content-Type': 'text/plain',
+}
+
+const handleLog = (req, res) => {
+ let body = ''
+ req.on('data', chunk => {
+ body += chunk.toString()
+ })
+
+ req.on('end', () => {
+ try {
+ const msg = JSON.parse(body)
+ msg.logclient = {
+ ip: req.connection.remoteAddress,
+ ua: req.headers['user-agent']
+ }
+ fileStream.write(JSON.stringify(msg))
+ fileStream.write('\n')
+
+ res.writeHead(200, 'ok', resHeaders)
+ res.end('ok')
+ } catch (err) {
+ console.error(err)
+ res.writeHead(400)
+ res.end('invalid payload')
+ }
+ });
+}
+
+const requestHandler = (req, res) => {
+ console.log(req.method, req.url)
+ switch(req.method) {
+ case 'OPTIONS':
+ res.writeHead(200, 'ok', resHeaders)
+ return res.end('ok')
+ case 'POST':
+ if (req.url === '/log') return handleLog(req, res)
+ default:
+ res.writeHead(404)
+ return res.end('not found')
+ }
+}
+
+const fileStream = fs.createWriteStream(logfile, 'utf-8')
+const server = http.createServer(requestHandler)
+
+server.listen(port, err => {
+ if (err) {
+ console.error(err)
+ process.exit(1)
+ }
+ console.log('listening on', port)
+ console.log('writing to', logfile)
+})