Issue-based recipes

The system provides a full context (issue details, environment) as the input and expects a specific object on the output. Example of a recipe for JIRA that populates specific fields:

!Create method

function create(context) {
    const issue = context.issue;
    const device = context.device;
    const platform = context.platform;
    let summary;

    // Change summary string based on the type of the issue
    if (issue.type == 'crash') {
        summary = `CRASH: ${issue.key}: ${issue.summary || 'No summary'}`
    } else {
        summary = `USER BUG: ${issue.key}: ${issue.summary || 'No summary'}`
    }

    let description = issue.description || '';
    if (description) {
        // Add two newlines to separate other data from the description
        description += '\n\n';
    }

    if (issue.reporter) {
        description += `Reported by ${issue.reporter}\n`;
    }

    description += `Device: ${device.manufacturer} ${device.model}\n`;
    description += `OS: ${platform.version} (${platform.build})\n`;


    description += `View full Bugsee session at: ${issue.url}`;

    return {
        summary: summary,
        description: description,
        description_format: 'markdown', // Optional
        custom: {
            components: [{id: 1234}], // Automatically set JIRA component
        }
    };
}

!Update method

function update(context, changes) {
    const issue = context.issue;
    const platform = context.platform;
    const device = context.device;
    const app = context.app;
    const result = {};

    if (changes.description) {
        let description = changes.description.to || '';

        if (description) {
            // Add two newlines to separate other data from the description
            description += '\n\n';
        }

        if (issue.reporter) {
            description += `Reported by ${issue.reporter}\n`;
        }

        description += `View full Bugsee session at: ${issue.url}`;

        result.description = description;
    }

    if (changes.summary) {
        // Change summary string based on the type of the issue
        if (issue.type == 'crash') {
            result.summary = `CRASH: ${issue.key}: ${changes.summary.to || 'No summary'}`
        } else {
            result.summary = `USER BUG: ${issue.key}: ${changes.summary.to || 'No summary'}`
        }
    }

    if (changes.state) {
        // Override state with a specific value, otherwise it will be mapped
        // automatically from Bugsee issue state ('open', 'closed')
        // result.state = 'completed';
    }

    return {
        issue: {
            description_format: 'markdown', // Optional
            custom: {
                // Optional
            }
        },
        changes: result
    };
}

The basic structure of a recipe is:

function create(context) {
    // ....

    return {
        // return object with fields that needs to be overridden
        // allowed properties: 'summary', 'description', 'reporter', 'priority', 'labels'
        custom: {
            // custom, service specific fields, refer to a documentation of a particular service for custom fields
            // for a specific service.
        }
    };
}

function update(context, changes) {
    const result = {};
    // ...


    if (changes.description) {
        // 'changes' object contains fields that were changed with old and new values ({ from: <old_value>, to: <new_value> }).
        // Simply ensure any of them is within 'changes' and react accordingly
    }

    return {
        issue: {
            custom: {
                // custom, service specific fields, refer to a documentation of a particular service for custom fields
                // for a specific service.
            }
        },
        changes: result
    };
}

Output customization

Services we integrate with, allow you to format the summary/description using some popular markup language - mostly these are Markdown and HTML. We handle these fields accordingly by escaping all the controlling entities in passed strings. This is required to keep the original strings intact.

Consider the following description line as an example:

Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[__NSDictionaryM removeObjectForKey:]: key cannot be nil'

If we pass that string to a service that pre-formats text using Markdown, then we potentially will loose some of the chars from that line (e.g. asterisks will be treated as bold triggers and will not be displayed).

To mitigate this, we let you set the description_format field. By default, its value is set to raw which means that target field is not formatted with any of the markup languages and contains a plain string. You can set it to either html or markdown depending on the target service formatting support. In our example above, if we set description_format to markdown and pass the result of the recipe to JIRA, description field will be kept intact. Setting the "_format" field instructs our algorithm to omit escaping the data if it matches the supported markup in remote service. Below is a table with all the services and what format they expect when is set in recipe result.

Service Supported markup
Aha html
Asana raw
Basecamp html
BitBucket markdown
BugZilla raw
Clubhouse markdown
Freshdesk html
GitHub markdown
GitLab markdown
Hipchat html
Jira markdown
Monday html
Nozbe markdown
Odoo html
Pivotal markdown
Redmine raw
Slack raw
Targetprocess html
Testlio raw
Trello raw
VSTS html
Wrike raw
YouTrack raw
Zendesk html
ZohoDesk raw
ZohoProjects raw

Context

The input to the create() and update() methods is a full context of the issue about to be created:

{
    integration: {
        provider: 'github'   // provider name that current recipe is executed for
    },
    org: {
        name: 'My Awesome Organization'
    },
    app: {
        _id: '5fe309e2b87c320a48000000', // ID of the application
        name: 'My Awesome App', // Application name
        version: '1.0.2',
        build: '12345'
    },
    issue: {
        _id: '5fe309e2b87c320a48000000', // ID of the issue
        key: 'IOS-123', // Unique key of the issue,
        summary: 'Something does not work', // Short summary of the issue
        description: '', // Description of the issue,
        created_on: '2017-01-19 23:29:32.021Z', // Date of creation
        type: 'bug', // Type of the issue, 'bug', 'crash' or 'error'
        state: 'open', // Issue state. Either 'open' or 'closed'
        labels: [], // String labels assigned to the issue
        url: 'https://app.bugsee.com/#/apps/IOS/issues/IOS-123', // URL of the issue
        shared_url: 'https://app.bugsee.com/#/issues/d504e2ba101b47509797eca7b7df229c', // URL for shared issue. Present only if issue sharing is enabled
        priority: 5, // Automatically guessed priority for the current remote service provider (JIRA, GitHub, etc.)
        severity: 5, // Original Bugsee severity (1 through 5)
        reporter: 'John Smith (john.smith@example.com)', // User who reported an issue
        attributes: {
            // Session/user attributes
        },
        attachments: [  // May be null if there are no attachments in the issue
            {
                name: 'additionalData',
                url: 'https://attachment.url/goes/here'
            }
        ],
        last_recording_id: '5fe309e2b87c320a48000000', // ID of the recently added recording (optional)
        recordings: 1 // Number of recordings within this issue. For bugs it's always equal 1.
        statistics: {
            total_count: 1, // Total number of occurrences 
            unique_devices: 1 // Total number of users (devices) affected by this issue
        }
    },
    device: {
        name: 'My precious',
        manufacturer: 'Samsung',
        model: 'GT-1656',
        model_name: 'Samsung Galaxy S6',
        screen: {
            width: 1440,
            height: 2560,
            scale: 1,
            dpiX: 577,
            dpiY: 577
        }
    },
    platform: {
        type: 'android', // Type: 'ios', 'android'
        version: '7.2.3', // OS version
        build: 'BG1234', // OS build
        release: 'Nougat', // Release name
        locale: 'en-US', // Locale
        locale_extended_info: {
            chosen_locales: ['en-US', 'de-GB'],
            display_locale: 'en-US',
            format_locale: 'en-US'
        }
    },
    // exists for manually pushed issues
    submitter: {
        name: 'John Doe',
        email: 'name@example.com'
    },
    // version and build of Bugsee SDK
    sdk: {
        version: '1.26.3',
        build: '10260cc-351'
    }
}

Changes

Method update(context, changes) along with context receives changes object. It has the following structure:

{
    "<field>": {
        "from": "<old_value>",
        "to": "<new_value>"
    }
}

changes object contains the fields (and their values) that were changed. Be aware, that all the changes are related to issue object in context. And in turn, context itself contains all the new values that are present in changes.

So, when implementing update flow, you should check whether some field was actually changed:

function update(context, changes) {
    const result = {};
    // ...

    if (changes.summary) {
        // summary was changed, update it here according to your needs
        result.summary = `UPD[summary]: ${changes.summary.from} -> ${changes.summary.to}`;
    }

    // ...
    return {
        issue: {
            custom: {}
        },
        changes: result
    };
}