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) +})