DEV Community

Cover image for Developing Ray Libraries
Patrick Organ
Patrick Organ

Posted on

Developing Ray Libraries

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.

ray-proxy-screenshot

Proxy Usage

Install ray-proxy as you would any other npm package:

npm install ray-proxy --save-dev
Enter fullscreen mode Exit fullscreen mode

First, set your port in the Ray app to 23516 in the preferences. Finally, run the proxy:

./node_modules/.bin/ray-proxy
Enter fullscreen mode Exit fullscreen mode

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>);
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

Payload sections overview

  • The "uuid" section contains a valid UUIDv4 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 to ray() 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.

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
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
{
  "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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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',
};
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
  },
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

Finally, make sure the Ray app is running and run the following command in your terminal:

node ./dist/test.js
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

Run the following, to see what we should be sending:

php ./ray-library-reference/ray-test.php
Enter fullscreen mode Exit fullscreen mode

And here's what SHOULD be sent (relevant parts only):

{
  ...
  "payloads": [
    {
      "type": "log",
      "content": {
        "values": [
          "Hello World!"
        ]
      },
    ...
    }
  ],
  "meta": {
    ...
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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>');
Enter fullscreen mode Exit fullscreen mode

image

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)