-
Notifications
You must be signed in to change notification settings - Fork 43
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #30 from zalmoxisus/v0.3
- Loading branch information
Showing
20 changed files
with
3,888 additions
and
155 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,3 +3,4 @@ node_modules | |
.DS_Store | ||
coverage | ||
.idea | ||
remotedev-db.sqlite3 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
{ | ||
"client": "sqlite3", | ||
"connection": { "filename": ":memory:" }, | ||
"pool": { | ||
"min": 1, | ||
"max": 1, | ||
"idleTimeoutMillis": 360000000, | ||
"disposeTimeout": 360000000 | ||
}, | ||
"useNullAsDefault": true, | ||
"debug": false, | ||
"migrate": true | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
## Realtime monitoring | ||
|
||
### WebSocket Clients | ||
|
||
We're using [SocketCluster](http://socketcluster.io/) for realtime communication, which provides a fast and scalable webSocket layer (via [`µWS`](https://github.com/uWebSockets/uWebSockets)) and a minimal pub/sub system. You need to include one of [its clients](https://github.com/SocketCluster/client-drivers) in your app to communicate with RemotedevServer. Currently there are clients for [JavaScript (NodeJS)](https://github.com/SocketCluster/socketcluster-client), [Java](https://github.com/sacOO7/socketcluster-client-java), [Python](https://github.com/sacOO7/socketcluster-client-python), [C](https://github.com/sacOO7/socketcluster-client-C), [Objective-C](https://github.com/abpopov/SocketCluster-ios-client) and [.NET/C#](https://github.com/sacOO7/SocketclusterClientDotNet). | ||
|
||
By default, the websocket server is running on `ws://localhost:8000/socketcluster/`. | ||
|
||
### Messaging lifecycle | ||
|
||
#### 1. Connecting to the WebSocket server | ||
|
||
The client driver provides a way to connect to the server via websockets (see the docs for the selected client). | ||
|
||
##### JavaScript | ||
```js | ||
var socket = socketCluster.connect({ | ||
hostname: 'localhost', | ||
port: 8000 | ||
}); | ||
``` | ||
|
||
##### Python | ||
```py | ||
socket = Socketcluster.socket("ws://localhost:8000/socketcluster/") | ||
socket.connect() | ||
``` | ||
|
||
> Note that JavaScript client composes the url from `hostname` and `port`, adding `/socketcluster/` path automatically. For other clients, you should specify that path. For example, for `ObjectiveC` it would be `self.client.initWithHost("localhost/socketcluster/", onPort: 8000, securely: false)`. | ||
#### 2. Disconnecting and reconnecting | ||
|
||
SocketCluster client handles reconnecting for you, but you still might want to know when the connection is established, or when it failed to connect. | ||
|
||
##### JavaScript | ||
```js | ||
socket.on('connect', status => { | ||
// Here will come the next step | ||
}); | ||
socket.on('disconnect', code => { | ||
console.warn('Socket disconnected with code', code); | ||
}); | ||
socket.on('error', error => { | ||
console.warn('Socket error', error); | ||
}); | ||
``` | ||
|
||
##### Python | ||
```py | ||
def onconnect(socket): | ||
// Here will call the next step | ||
|
||
def ondisconnect(socket): | ||
logging.info("on disconnect got called") | ||
|
||
def onConnectError(socket, error): | ||
logging.info("On connect error got called") | ||
|
||
socket.setBasicListener(onconnect, ondisconnect, onConnectError) | ||
``` | ||
|
||
#### 3. Authorizing and subscribing to the channel of events | ||
|
||
We're not providing an authorizing mechanism yet. All you have to do is to emit a `login` event, and you'll get a `channelName` you should subscribe for, and watch for messages and events. Make sure to pass the `master` event, otherwise it should be a monitor, not a client app. | ||
|
||
##### JavaScript | ||
```js | ||
socket.emit('login', 'master', (error, channelName) => { | ||
if (error) { console.log(error); return; } | ||
channel = socket.subscribe(channelName); | ||
channel.watch(handleMessages); | ||
socket.on(channelName, handleMessages); | ||
}); | ||
|
||
function handleMessages(message) { | ||
// 5. Listening for monitor events | ||
} | ||
``` | ||
|
||
##### Python | ||
```py | ||
socket.emitack("login", "master", login) | ||
|
||
def login(key, error, channelName): | ||
socket.subscribe(channelName) | ||
socket.onchannel(channelName, handleMessages) | ||
socket.on(channelName, handleMessages) | ||
|
||
def handleMessages(key, message): | ||
// 5. Listening for monitor events | ||
``` | ||
|
||
You could just emit the `login` event, and omit subscribing (and point `5` bellow) if you want only to log data, not to interact with te app. | ||
|
||
#### 4. Sending the action and state to the monitor | ||
|
||
To send your data to the monitor use `log` or `log-noid` channel. The latter will add the socket id to the message from the server side (useful when the message was sent before the connection was established). | ||
|
||
The message object includes the following: | ||
- `type` - usually should be `ACTION`. If you want to indicate that we're starting a new log (clear all actions emitted before and add `@@INIT`), use `INIT`. In case you have a lifted state similar to one provided by [`redux-devtools-instrument`](https://github.com/zalmoxisus/redux-devtools-instrument), use `STATE`. | ||
- `action` - the action object. It is recommended to lift it in another object, and add `timestamp` to show when the action was fired off: `{ timestamp: Date.now(), action: { type: 'SOME_ACTION' } }`. | ||
- `payload` - usually the state or lifted state object. | ||
- `name` - name of the instance to be shown in the instances selector. If not provided, it will be equal to `instanceId`. | ||
- `instanceId` - an id to identify the instance. If not provided, it will be the same as `id`. However, it is useful when having several instances (or stores) in the same connection. Also if the user will specify a constant value, it would allow to persist the state on app reload. | ||
- `id` - socket connection id, which should be either `socket.id` or should not provided and use `log-noid` channel. | ||
|
||
##### JavaScript | ||
```js | ||
const message = { | ||
type: 'ACTION', | ||
action: { action, timestamp: Date.now() }, | ||
payload: state, | ||
id: socket.id, | ||
instanceId: window.btoa(location.href), | ||
name: document.title | ||
}; | ||
socket.emit(socket.id ? 'log' : 'log-noid', message); | ||
``` | ||
|
||
##### Python | ||
```py | ||
class Message: | ||
def __init__(self, action, state): | ||
self.type = "ACTION" | ||
self.action = action | ||
self.payload = state | ||
id: socket.id | ||
socket.emit(socket.id if "log" else "log-noid", Message(action, state)); | ||
``` | ||
|
||
#### 5. Listening for monitor events | ||
|
||
When a monitor action is emitted, you'll get an event on the subscribed function. The argument object includes a `type` key, which can be: | ||
- `DISPATCH` - a monitor action dispatched on Redux DevTools monitor, like `{ type: 'DISPATCH', payload: { type: 'JUMP_TO_STATE', 'index': 2 }`. See [`redux-devtools-instrument`](https://github.com/zalmoxisus/redux-devtools-instrument/blob/master/src/instrument.js) for details. Additionally to that API, you'll get also a stringified `state` object when needed. So, for example, for time travelling (`JUMP_TO_STATE`) you can just parse and set the state (see the example). Usually implementing this type of actions would be enough. | ||
- `ACTION` - the user requested to dispatch an action remotely like `{ type: 'ACTION', action: '{ type: \'INCREMENT_COUNTER\' }' }`. The `action` can be either a stringified javascript object which should be evalled or a function which arguments should be evalled like [here](https://github.com/zalmoxisus/remotedev-utils/blob/master/src/index.js#L62-L70). | ||
- `START` - a monitor was opened. You could handle this event in order not to do extra tasks when the app is not monitored. | ||
- `STOP` - a monitor was closed. You can take this as no need to send data to the monitor. I there are several monitors and one was closed, all others will send `START` event to acknowledge that we still have to send data. | ||
|
||
See [`mobx-remotedev`](https://github.com/zalmoxisus/mobx-remotedev/blob/master/src/monitorActions.js) for an example of implementation without [`redux-devtools-instrument`](https://github.com/zalmoxisus/redux-devtools-instrument/blob/master/src/instrument.js). | ||
|
||
##### JavaScript | ||
```js | ||
function handleMessages(message) { | ||
if (message.type === 'DISPATCH' && message.payload.type === 'JUMP_TO_STATE') { | ||
store.setState(JSON.parse(message.state)); | ||
} | ||
} | ||
``` | ||
|
||
##### Python | ||
```py | ||
def handleMessages(key, message): | ||
if message.type === "DISPATCH" and message.payload.type === "JUMP_TO_STATE": | ||
store.setState(json.loads(message.state)); | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
var makeExecutableSchema = require('graphql-tools').makeExecutableSchema; | ||
var requireSchema = require('../utils/requireSchema'); | ||
var schema = requireSchema('./schema_def.graphql', require); | ||
|
||
var resolvers = { | ||
Query: { | ||
reports: function report(source, args, context, ast) { | ||
return context.store.listAll(); | ||
}, | ||
report: function report(source, args, context, ast) { | ||
return context.store.get(args.id); | ||
} | ||
} | ||
}; | ||
|
||
var executableSchema = makeExecutableSchema({ | ||
typeDefs: schema, | ||
resolvers: resolvers | ||
}); | ||
|
||
module.exports = executableSchema; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
# A list of options for the type of the report | ||
enum ReportType { | ||
STATE | ||
ACTION | ||
STATES | ||
ACTIONS | ||
} | ||
|
||
type Report { | ||
# Report ID | ||
id: ID! | ||
# Type of the report, can be: STATE, ACTION, STATES, ACTIONS | ||
type: ReportType, | ||
# Briefly what happened | ||
title: String, | ||
# Details supplied by the user | ||
description: String, | ||
# The last dispatched action before the report was sent | ||
action: String, | ||
# Stringified actions or the state or both, which should be loaded the application to reproduce the exact behavior | ||
payload: String, | ||
# Stringified preloaded state object. Could be the initial state of the app or committed state (after dispatching COMMIT action or reaching maxAge) | ||
preloadedState: String, | ||
# Screenshot url or blob as a string | ||
screenshot: String, | ||
# User Agent String | ||
userAgent: String, | ||
# Application version to group the reports and versioning | ||
version: String, | ||
# Used to identify the user who sent the report | ||
userId: String, | ||
# More detailed data about the user, usually it's a stringified object | ||
user: String, | ||
# Everything else you want to send | ||
meta: String, | ||
# Error message which invoked sending the report | ||
exception: String, | ||
# Id to identify the store in case there are multiple stores | ||
instanceId: String, | ||
# Timestamp when the report was added | ||
added: String | ||
# Id to identify the application (from apps table) | ||
appId: ID | ||
} | ||
|
||
# Explore GraphQL query schema | ||
type Query { | ||
# List all reports | ||
reports: [Report] | ||
# Get a report by ID | ||
report( | ||
# Report ID | ||
id: ID! | ||
): Report | ||
} | ||
|
||
schema { | ||
query: Query | ||
#mutation: Mutation | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
var path = require('path'); | ||
var knexModule = require('knex'); | ||
|
||
module.exports = function connector(options) { | ||
var dbOptions = options.dbOptions; | ||
dbOptions.useNullAsDefault = true; | ||
if (!dbOptions.migrate) { | ||
return knexModule(dbOptions); | ||
} | ||
|
||
dbOptions.migrations = { directory: path.resolve(__dirname, 'migrations') }; | ||
dbOptions.seeds = { directory: path.resolve(__dirname, 'seeds') }; | ||
var knex = knexModule(dbOptions); | ||
|
||
knex.migrate.latest() | ||
.then(function() { | ||
return knex.seed.run(); | ||
}) | ||
.then(function() { | ||
console.log(' \x1b[0;32m[Done]\x1b[0m Migrations are finished\n'); | ||
}) | ||
.catch(function(error) { | ||
console.error(error); | ||
}); | ||
|
||
return knex; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
exports.up = function(knex, Promise) { | ||
return Promise.all([ | ||
knex.schema.createTable('remotedev_reports', function(table) { | ||
table.uuid('id').primary(); | ||
table.string('type'); | ||
table.string('title'); | ||
table.string('description'); | ||
table.string('action'); | ||
table.text('payload', 'longtext'); | ||
table.text('preloadedState', 'longtext'); | ||
table.text('screenshot', 'longtext'); | ||
table.string('userAgent'); | ||
table.string('version'); | ||
table.string('user'); | ||
table.string('userId'); | ||
table.string('instanceId'); | ||
table.string('meta'); | ||
table.string('exception'); | ||
table.timestamp('added').defaultTo(knex.fn.now()); | ||
table.uuid('appId') | ||
.references('id') | ||
.inTable('remotedev_apps').onDelete('CASCADE').onUpdate('CASCADE') | ||
.defaultTo('78626c31-e16b-4528-b8e5-f81301b627f4'); | ||
}), | ||
knex.schema.createTable('remotedev_payloads', function(table){ | ||
table.uuid('id').primary(); | ||
table.text('state'); | ||
table.text('action'); | ||
table.timestamp('added').defaultTo(knex.fn.now()); | ||
table.uuid('reportId') | ||
.references('id') | ||
.inTable('remotedev_reports').onDelete('CASCADE').onUpdate('CASCADE'); | ||
}), | ||
knex.schema.createTable('remotedev_apps', function(table){ | ||
table.uuid('id').primary(); | ||
table.string('title'); | ||
table.string('description'); | ||
table.string('url'); | ||
table.timestamps(false, true); | ||
}), | ||
knex.schema.createTable('remotedev_users', function(table){ | ||
table.uuid('id').primary(); | ||
table.string('name'); | ||
table.string('login'); | ||
table.string('email'); | ||
table.string('avatarUrl'); | ||
table.string('profileUrl'); | ||
table.string('oauthId'); | ||
table.string('oauthType'); | ||
table.string('token'); | ||
table.timestamps(false, true); | ||
}), | ||
knex.schema.createTable('remotedev_users_apps', function(table){ | ||
table.boolean('readOnly').defaultTo(false); | ||
table.uuid('userId'); | ||
table.uuid('appId'); | ||
table.primary(['userId', 'appId']); | ||
table.foreign('userId') | ||
.references('id').inTable('remotedev_users').onDelete('CASCADE').onUpdate('CASCADE'); | ||
table.foreign('appId') | ||
.references('id').inTable('remotedev_apps').onDelete('CASCADE').onUpdate('CASCADE'); | ||
}) | ||
]) | ||
}; | ||
|
||
exports.down = function(knex, Promise) { | ||
return Promise.all([ | ||
knex.schema.dropTable('remotedev_reports'), | ||
knex.schema.dropTable('remotedev_apps') | ||
]) | ||
}; |
Oops, something went wrong.