Ray is a beautiful, lightweight desktop app that helps you debug your app. It supports PHP, Ruby, JavaScript, TypeScript, NodeJS and Bash applications.
It can, in fact, support any language for which there is an integration library.
When developing a Ray Library, you may find it necessary to debug the data being sent to the Ray app.
You may use the third-party package permafrost-dev/ray-proxy to intercept and display the data being sent from your code to the Ray app.
Proxy Usage
Install ray-proxy
as you would any other npm package:
npm install ray-proxy --save-dev
First, set your port in the Ray app to 23516
in the preferences. Finally, run the proxy:
./node_modules/.bin/ray-proxy
Once you start sending ray messages, you'll see the raw payloads being sent in the proxy, along with statistics!
Structure of a Payload
Every time any data is sent to Ray, a payload is generated with the same basic structure before being sent to the Ray app.
If you run the following test PHP script, you'll get the payload example below sent via HTTP POST
to the Ray app.
ray()->html('<em>hello world!</em>);
Payload example
{
"uuid": "ca539a10-bfd5-3e5a-6271-0c4a95612132",
"payloads": [
{
"type": "custom",
"content": {
"content": "<em>hello world!</em>",
"label": "HTML"
},
"origin": {
"function_name": "test",
"file": "/home/user/projects/test-project/test.php",
"line_number": 16,
"hostname": "my-hostname"
}
}
],
"meta": {
"php_version": "7.4.16",
"php_version_id": 70416,
"ray_package_version": "1.20.1.0"
}
}
Payload sections overview
- The
"uuid"
section contains a validUUIDv4
value. This value is important, as it can be used in future to modify the payload's display, such as changing its color. - The
"meta"
section contains metadata about the Ray integration library as well as the current language and its version. - The
payloads[0].origin
section contains information about where the call toray()
originated from. It's used to tell Ray what file to open when the related file link is clicked. - The
payloads[0].type
value contains the type of payload being sent. - The
payloads[0].content
value contains the payload-specific content to display within the Ray app. This varies depending on the type of payload.
Developing a Ray Library
The Ray app is not a language-specific debugging app - as long as there's an integration library, it can be used with any language.
If you're interested in developing a Ray library for your language of choice, this document will help guide you through the process. As an example, we'll be creating an example Ray integration library for Javascript/NodeJS; however, the concepts apply to any language.
- Estimated time investment:
1 hour
- Required Tools: PHP 7.4, Typescript, ESBuild, ray-proxy
- Companion Repository: permafrost-dev/creating-a-ray-integration
Creating a javascript integration for Ray
In this guide, we'll create a new javascript library that communicates with Ray. There are already comprehensive third-party libraries in this space, such as node-ray, but this will serve well as an example.
Goals
Create a javascript library that communicates with Ray that implements the following methods:
color()
html()
ray()
charles()
send()
sendRequest()
Getting Started
The spatie/ray
PHP package should be used as a reference - it is the primary library for Ray, and all new functionality is always added here first. We'll reference its source code as we write our library.
Create a new directory named ray-library-reference
, and run:
cd ./ray-library-reference
composer init
composer require spatie/ray
Next, create a PHP script for testing:
<?php
// ray-test.php
require_once(__DIR__.'/vendor/autoload.php');
ray('test one')->color('red');
ray()->html('<strong>this is a bold</string>')->color('blue');
ray()->send('this is a test');
Tools
To help determine what payload data is actually being sent to Ray from our test PHP script, we'll use the third-party permafrost-dev/ray-proxy
package to intercept and display all payloads being sent to Ray.
You'll first need to install the ray-proxy
package:
mkdir ./ray-lib-app
cd ./ray-lib-app
npm install ray-proxy
When you're ready to start intercepting data, start Ray app and set the port to 23516
. Then start the proxy:
node ./node_modules/.bin/ray-proxy
Technology/Package choices
For the development, we'll use TypeScript as the primary language and the esbuild
package to compile and bundle our library.
We'll use the superagent
npm package for sending data to Ray, and the uuid
package for generating the required UUIDv4
values for creating valid payloads.
cd ./ray-lib-app
npm install --save-dev typescript esbuild
npm install superagent uuid
Next, let's set up our project:
mkdir ./src
mkdir ./dist
touch ./dist/test.js
touch ./src/Origin.ts
touch ./src/Ray.ts
touch ./src/payloadUtils.ts
Structure of a Payload
Let's start with the raw contents of a payload that is sent from your code to the Ray app. You may view it in ray-proxy
by running the following:
php ./ray-library-reference/ray-test.php
{
"uuid": "ca539a19-afd7-4c5e-8142-0d4b94512241",
"payloads": [
{
"type": "custom",
"content": {
"content": "string 1 1615239018342",
"label": "HTML"
},
"origin": {
"function_name": "test",
"file": "/home/user/projects/test-project/test.js",
"line_number": 16,
"hostname": "my-hostname"
}
}
],
"meta": {
"php_version": "7.4.16",
"php_version_id": 70416,
"ray_package_version": "1.20.1.0"
}
}
Here we see that a payload has 3 parts: the type
, the content
, and the origin
. It appears that multiple payloads can be sent at once (because the payloads
property is an array).
Sent along with the payload is a UUIDv4
and a meta
object, which seems to contain the name and version of the Ray library we're using.
Since this is a basic walk though, we'll use hard-coded Origin
data - data about where the call originated from - placed in ./src/Origin.ts
(it is left to the reader to implement a working Origin class):
// src/Origin.ts
export const OriginData = {
function_name: 'my_test_func',
file: 'my-file.js',
line_number: 16,
hostname: 'my-hostname',
};
Then create ./src/payloadUtils.ts
, which will contain helper functions for creating payloads:
// src/payloadUtils.ts
import { v4 as uuidv4 } from 'uuid';
import { OriginData } from './Origin';
export function createSendablePayload(payloads: any[] = [], uuid: string | null = null): any {
uuid = uuid ?? uuidv4({}).toString();
return { uuid, payloads, meta: { my_package_version: "1.0.0" } };
}
export function createPayload(type: string, label: string | undefined, content: any, contentName: string = 'content'): any {
let result = {
type: type,
content: {
[contentName]: content,
label: label,
},
origin: OriginData,
};
if (result.content.label === undefined) {
delete result.content['label'];
}
return result;
}
// change the color of a previously sent payload in Ray
export function createColorPayload(colorName: string, uuid: string | null = null) {
const payload = createPayload('color', undefined, colorName, 'color');
return createSendablePayload([payload], uuid);
}
// create an "HTML" payload to display custom HTML in Ray
export function createHtmlPayload(htmlContent: string, uuid: string | null = null) {
const payload = createPayload('custom', 'HTML', htmlContent);
return createSendablePayload([payload], uuid);
}
// create a "log" payload to display basic text in Ray
export function createLogPayload(text: string|string[], uuid: string | null = null) {
const payload = createPayload('log', 'log', text);
return createSendablePayload([payload], uuid);
}
Now, we'll need our main class - ./src/Ray.ts
:
import { createLogPayload, createColorPayload, createHtmlPayload } from './payloadUtils';
const superagent = require('superagent');
export class Ray {
public uuid: string | null = null;
public color(name: string): Ray {
const payload = createColorPayload(name, this.uuid);
return this.sendRequest(payload);
}
public html(name: string): Ray {
const payload = createHtmlPayload(name, this.uuid);
return this.sendRequest(payload);
}
public send(...args: any[]): Ray {
args.forEach(arg => {
const payload = createLogPayload('log', null, arg);
this.sendRequest(payload);
});
return this;
}
public ban(): Ray {
return this.send('🕶');
}
public charles(): Ray {
return this.send('🎶 🎹 🎷 🕺');
}
public sendRequest(request: any): Ray {
this.uuid = request.uuid;
superagent.post(`http://localhost:23517/`).send(request)
.then(resp => { })
.catch(err => {});
return this;
}
}
export default Ray;
Building the library
We'll be using ESBuild
to compile our library, which is a very fast ECMA compiler built in golang.
The following command tells ESBuild
to bundle all files into a single output file, that it will be run on the node
platform (instead of in a browser), to target node v12 as the minimum node version to support, and to treat the superagent
npm package as external (meaning it should not be packaged as part of our outfile).
./node_modules/.bin/esbuild --bundle \
--target=node12 --platform=node \
--format=cjs --external:superagent \
--outfile=dist/index.js src/Ray.ts
if you'd like to add a shortcut, modify the scripts
section in your package.json
file to the following:
"scripts": {
"build": "./node_modules/.bin/esbuild --bundle --target=node12 --platform=node --format=cjs --external:superagent --outfile=dist/index.js src/Ray.ts"
},
Once saved, you may run the npm run build
command instead of the ./node_modules/.bin/esbuild ...
command.
After running the build command you choose, you'll see that the file ./dist/index.js
has been created.
Finally, we're ready to test our library.
Testing the library
First, edit the file named ./dist/test.js
:
// ./dist/test.js
const { Ray } = require('./index');
(new Ray()).html('<em>hello world</em>').color('red');
(new Ray()).send('Hello World!').color('blue');
Finally, make sure the Ray app is running and run the following command in your terminal:
node ./dist/test.js
With any luck, you'll see the message "hello world" with a red marker next to it in the Ray app - but not the "hello world 2" blue message.
Debugging Issues
If you were to test the send()
method, you'd notice that nothing appears in the Ray app. No problem, though - since we've got access to the PHP package as well as the ray-proxy
app running, we can debug this in no time.
Here's what is being sent from our library to Ray, according to the proxy:
{
"uuid": "8bd2e386-e000-47b6-bd31-a655fd66376d",
"payloads": [
{
"type": "log",
"content": {
"content": "log",
"label": "log"
},
"origin": {
"function_name": "my_test_func",
"file": "my-file.js",
"line_number": 16,
"hostname": "my-hostname"
}
}
],
"meta": {
"my_package_version": "1.0.0"
}
}
Run the following, to see what we should be sending:
php ./ray-library-reference/ray-test.php
And here's what SHOULD be sent (relevant parts only):
{
...
"payloads": [
{
"type": "log",
"content": {
"values": [
"Hello World!"
]
},
...
}
],
"meta": {
...
}
}
The issue appears to be with our createLogPayload
function, so let's change a few things:
export function createLogPayload(text: string|string[], uuid: string | null = null) {
// make sure we pass an array of values
if (!Array.isArray(text)) {
text = [text];
}
// add the last parameter to send "content.values" in the payload
const payload = createPayload('log', 'log', text, 'values');
return createSendablePayload([payload], uuid);
}
And lastly, we need to update the send()
method on the Ray
class:
public send(...args: any[]): Ray {
// we only need to send a single payload with `args` as the data, instead of
// sending a new payload for each arg in args.
const payload = createLogPayload(args, this.uuid);
this.sendRequest(payload);
return this;
}
Let's compile again, and re-run our test script:
./node_modules/.bin/esbuild --bundle \
--target=node12 --platform=node \
--format=cjs --external:superagent \
--outfile=dist/index.js src/Ray.ts
node dist/test.js
Success! You should see two "hello world" messages, one red and one blue.
Let's make one more change to make using our library easier: adding a ray()
function to ./src/Ray.ts
:
// ... Ray class code here
export function ray(...args: any[]) {
return (new Ray()).send(...args);
}
export default Ray;
You can now modify your test script to something like:
const { ray } = require('./index');
ray('hello world').color('red');
ray().html('<strong>bold text</strong>');
That's it! Make sure to take a look at the companion repository to check out the final project code.
Don't forget to stop the
ray-proxy
app and change your Ray port back to 23517.
Top comments (0)