Sharing Console Log from Ionic Apps
Logging to browser console can be a useful tool for troubleshooting Ionic applications during development. Although all the logging is still done in test builds, the process of getting the log from the device is not tester-friendly:
- Connect the phone to the computer and make sure it's a Mac when testing on iOS.
- Attach to the application from the desktop browser.
- Copy the log from the browser console and include it in the bug report.
If the testing can't be done in the office, even having constant access to a computer can already be a challenge.
Without too much effort this experience can be improved a lot by adding an option to export the log directly from the application. It only takes three steps.
Intercepting Calls to Console
Once the log messages are written to the browser console, they can't be accessed programmatically any more. To make them accessible at a later time, they need to be intercepted when logged. Since we're using TypeScript, we can use monkey patching to replace the built-in console functions. Toby Ho wrote an excellent blogpost about this topic. We can use a slightly modified version of his code:
private interceptConsole() {
let interceptFn = (method: string) => {
let original = console[method];
console[method] = function() {
// do our own logging here
if (original.apply) {
original.apply(console, arguments);
} else {
let message = Array.prototype.slice.apply(arguments).join(' ');
original(message);
}
};
};
['log', 'warn', 'error'].forEach(method => {
interceptFn(method);
});
}
We only need to call this method once at application startup, e.g. inside the app.component.ts
constructor.
Storing Log Messages
We need to store the intercepted messages somewhere so that we can retrieve them again later. If there aren't too many messages and we only need to persist them while the application is running, we can use an in-memory array:
import { Injectable } from '@angular/core';
@Injectable()
export class LoggingProvider {
private messages: Array<string> = [];
log(...messages: Array<any>) {
this.messages.push(...messages);
}
}
The spread operator is used to handle the variable number of arguments which can be passed to console
functions. Since not only strings but also objects can be logged, it makes sense to serialize them beforehand to get as much information as possible:
log(...messages: Array<any>) {
let serialized = messages.map(message => {
if (typeof message === 'object') {
try {
return JSON.stringify(message);
} catch (error) { }
}
return message;
});
this.messages.push(...messages);
}
The serialization process can be improved even further:
- By passing a
replacer
function toJSON.stringify
, it's possible to serialize additional information fromError
objects. - By using CircularJSON instead of
JSON.serialize
, even objects with circular references can be serialized.
Of course, we still need to import LoggingProvider
in app.component.ts
and call our new logging method from interceptConsole
:
private interceptConsole() {
let interceptFn = (method: string) => {
let original = console[method];
let loggingProvider = this.loggingProvider;
console[method] = function() {
loggingProvider.log.apply(loggingProvider, arguments);
if (original.apply) {
original.apply(console, arguments);
} else {
let message = Array.prototype.slice.apply(arguments).join(' ');
original(message);
}
};
};
['log', 'warn', 'error'].forEach(method => {
interceptFn(method);
});
}
Exporting the Log from the App
To export stored log messages we will use the Cordova Social Sharing Plugin and its Ionic Native wrapper. Since the log will quickly grow too large to share as text, we will store it to a file and share that instead:
import { SocialSharing } from '@ionic-native/social-sharing';
import { File } from '@ionic-native/file';
import { Injectable } from '@angular/core';
@Injectable()
export class LoggingProvider {
private filename = 'console.log';
private messages: Array<string> = [];
constructor(private file: File, private social: SocialSharing) { }
// log method omitted
share() {
this.file.writeFile(this.file.dataDirectory, this.filename,
this.messages.join('\r\n'), {replace: true}).then(() => {
this.file.resolveDirectoryUrl (this.file.dataDirectory).then(directory => {
this.file.getFile(directory, this.filename,
{ create: true, exclusive: false }).then(file => {
this.social.share('Message', 'Subject', file.toURL());
});
});
});
}
}
To expose the functionality in the application UI just bind the method to a button, e.g. on the About page. Of course, you can always hide the button and not invoke interceptConsole
in the final production build by using a flag, if you don't want to expose this functionality there.