diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..921ea0a --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,78 @@ +# 🤖 AGENTS.md + +## Purpose +This file defines the operating model for AI agents and human contributors working on this repository. It establishes **goals, scope, and guardrails** so that automation produces consistent, high-quality outputs without scope creep or errors. + +--- + +## 🎯 Core Objectives +- Maintain **accuracy and consistency** across all documentation in this repo. +- Ensure **Codelabs**, **Cookbooks**, and **CLI docs** follow standardized workflows and DevRev guidelines. +- Automate repetitive editing tasks while respecting repo boundaries. + +--- + +## 📂 Scope of Work + +### ✅ Allowed Directories +- `codelabs/` → 15 Codelabs, must follow standardized **Setup → Code → Run → Verify** structure. + +### ❌ Out of Scope +- Do not generate or modify **source code** in `code/` directories. +- Do not change **infrastructure configs** (`package.json`, `.github/`, `vercel.json`, etc.) unless explicitly instructed. +- Do not use **external knowledge** about DevRev or Snap-ins. Documentation must derive from repo content. + +--- + +## 🧭 Operating Guidelines + +### Documentation Rules +- Always include **frontmatter** (title, description). +- Start visible content at **H2**. +- Use **Setup, Code, Run, Verify** in all Codelabs. +- Provide **full, untruncated code snippets**. +- Add **expected output** in Verify sections. + +### Writing Standards +- **Voice**: Developer-first, active, concise. +- **Terminology**: Use correct DevRev capitalization (`DevRev`, `snap-in`, `manifest.yaml`). +- **Formatting**: + - Backticks for commands, file paths, code. + - Lists for sequential actions. + - Callouts for tips, warnings, errors. + +--- + +## 📋 Validation Protocol +Before finalizing changes, agents must confirm: +- [ ] All Codelabs have Setup → Code → Run → Verify flow. +- [ ] Code snippets and manifests are complete. +- [ ] Init, validate-manifest, and fixture creation covered in Setup. +- [ ] Terminology is consistent with DevRev style. +- [ ] Internal/external links are valid. +- [ ] No speculative or external content added. + +--- + +## 🛡️ Guardrails +- **Ground Truth**: All factual claims must be based on files in this repo. +- **No Speculation**: If information is missing, leave a placeholder or flag for human review. +- **Consistency First**: Enforce standard formats across all docs. +- **Evidence Required**: Cite file paths and line numbers when referencing code. + +--- + +## 🚀 Execution Flow +1. Identify target directory (`codelabs/`). +2. Apply **scope + writing guidelines**. +3. Revise or create documentation using repo content. +4. Run through **Validation Protocol**. +5. Commit with a descriptive message (e.g., `docs: revise codelab 03 with standardized structure`). + +--- + +## 📌 Success Definition +- 100% of Codelabs revised to standardized format. +- Cookbook entries are short, targeted, and runnable. +- CLI docs map directly to actual code in repo. +- All docs are actionable, accurate, and consistent. diff --git a/codelabs/1-starter.mdx b/codelabs/1-starter.mdx new file mode 100644 index 0000000..f97e9bb --- /dev/null +++ b/codelabs/1-starter.mdx @@ -0,0 +1,273 @@ +--- +title: 'Starter Snap-in' +description: 'A basic template for creating your own DevRev snap-ins, demonstrating fundamental structure and function registration.' +--- + +## Setup + +This section guides you on setting up a new snap-in project and explains the structure of this example. + +### Prerequisites + +- Node.js and `npm` installed. +- A DevRev account with the CLI installed and configured. + +### 1. Initialize Your Project + +To create a new snap-in, run the following command in your terminal, replacing `` with your desired project name: + +```bash +devrev snap_in_version init +``` + +This creates a new directory with a template structure for your snap-in. + +### 2. Validate the Manifest + +Before writing any code, it's a good practice to validate the template's `manifest.yaml` file. Run the following command from your project's root directory: + +```bash +devrev snap_in_version validate-manifest manifest.yaml +``` + +### 3. Prepare Test Data + +For local testing, you need a sample event payload. This example includes a fixture file at `code/src/fixtures/function_1_event.json`. You can create similar files for your own functions. + +## Code + +The core logic of your snap-in resides in its functions. This starter example includes a basic function that logs the event payload it receives. The code is located in `1-starter/code/src/functions/function_1/index.ts`. + +```typescript +/* + * Copyright (c) 2023 DevRev, Inc. All rights reserved. + */ + +export const run = async (events: any[]) => { + /* + Put your code here and remove the log below + */ + + console.info('events', events); +}; + +export default run; +``` + +This example uses a function factory pattern (`src/function-factory.ts`) to dynamically load and execute functions. This allows you to add new functions without modifying the core logic of the snap-in. + +## Run + +To run the function locally, navigate to the `1-starter/code` directory and execute the following commands. + +1. **Install dependencies:** + ```bash + npm install + ``` + +2. **Run the local test runner:** + ```bash + npm run start:watch -- --functionName=function_1 --fixturePath=function_1_event.json + ``` + +## Verify + +After running the commands, you should see the following output in your console, confirming that the function executed successfully. The output contains the full event payload from the `function_1_event.json` fixture. + +```json +info: events [ + { + "context": { + "dev_oid": "don:identity:dvrv-us-1:devo/0", + "automation_id": "don:integration:dvrv-us-1:devo/0:automation/00000001-0001-0001-0001-00000001", + "snap_in_id": "don:integration:dvrv-us-1:devo/0:snap_in/00000001-0001-0001-0001-00000001", + "snap_in_version_id": "don:integration:dvrv-us-1:devo/0:snap_in_package/00000001-0001-0001-0001-00000001:snap_in_version/00000001-0001-0001-0001-00000001" + }, + "execution_metadata": { + "request_id": "4QtCBSKJcKKqwQhoJKZvRQ", + "function_name": "foobar" + }, + "input_data": { + "global_values": { + "message": "tokens", + "ticket_id": "don:core:dvrv-us-1:devo/0:product/1" + }, + "event_sources": {}, + "keyrings": { + "devrev" : "" + } + }, + "payload": { + "id": "don:integration:dvrv-us-1:devo/0:webhook/WRVqEXT7:webhook_event/31WPF0QWh8M", + "timestamp": "2023-02-07T10:08:42.591611Z", + "type": "work_updated", + "unique_key": "ZG9uOmludGVncmF0aW9uOmR2cnYtdXMtMTpkZXZvLzhtNDZjcDdSOmV2ZW50X3NvdXJjZS8zMjAxMDIzOS00MjA5LTRjOGEtYjcyMy1hYmZkYjAyMzkxOGE=", + "webhook_id": "don:integration:dvrv-us-1:devo/0:webhook/WRVqEXT7", + "work_updated": { + "old_work": { + "applies_to_part": { + "display_id": "FEAT-5", + "id": "don:core:dvrv-us-1:devo/0:feature/5", + "id_v1": "don:DEV-0:feature:5", + "name": "Default Feature 5", + "type": "feature" + }, + "body": "Install the PLuG widget into your application with just a few lines of code and immediately bring the voice of your customer to your entire team. \n\nYou can also test the PLuG widget by clicking DevRev Org settings (top left DevRev icon) -> Support -> Try out PLuG.\n\nFollow step by step guide and copy and paste code from the link here -> https://devrev.ai/docs/plug/installation", + "created_by": { + "display_handle": "devrev-bot", + "display_id": "SYSU-1", + "display_name": "devrev-bot", + "full_name": "DevRev Bot", + "id": "don:identity:dvrv-us-1:devo/0:sysu/1", + "id_v1": "don:DEV-0:sys_user:SYSU-1", + "type": "sys_user" + }, + "created_date": "2023-01-31T12:04:25.946Z", + "custom_fields": null, + "display_id": "ISS-12", + "id": "don:core:dvrv-us-1:devo/0:issue/12", + "id_v1": "don:DEV-0:issue:12", + "links": [ + { + "link_id": "don:core:dvrv-us-1:devo/0:link/11", + "link_id_v1": "don:DEV-0:link:11", + "link_type": "is_dependency_of", + "target": "don:core:dvrv-us-1:devo/0:ticket/4", + "target_object_type": "ticket", + "target_v1": "don:DEV-0:ticket:4" + } + ], + "modified_by": { + "display_handle": "devrev-bot", + "display_id": "SYSU-1", + "display_name": "devrev-bot", + "full_name": "DevRev Bot", + "id": "don:identity:dvrv-us-1:devo/0:sysu/1", + "id_v1": "don:DEV-0:sys_user:SYSU-1", + "type": "sys_user" + }, + "modified_date": "2023-01-31T12:04:49.215Z", + "owned_by": [ + { + "display_handle": "devrev-bot", + "display_id": "SYSU-1", + "display_name": "devrev-bot", + "full_name": "DevRev Bot", + "id": "don:identity:dvrv-us-1:devo/0:sysu/1", + "id_v1": "don:DEV-0:sys_user:SYSU-1", + "type": "sys_user" + } + ], + "priority": "p1", + "stage": { + "name": "next", + "ordinal": 3000 + }, + "state": "open", + "stock_schema_fragment": "don:core:dvrv-us-1:stock_sf/292711", + "tags": [ + { + "id": { + "display_id": "TAG-1", + "id": "don:core:dvrv-us-1:devo/0:tag/1", + "id_v1": "don:DEV-0:tag:1", + "name": "" + }, + "tag": { + "display_id": "TAG-1", + "id": "don:core:dvrv-us-1:devo/0:tag/1", + "id_v1": "don:DEV-0:tag:1", + "name": "" + } + } + ], + "title": "'Install PLuG Today!' - Needs Dev Attention", + "type": "issue" + }, + "work": { + "applies_to_part": { + "display_id": "FEAT-5", + "id": "don:core:dvrv-us-1:devo/0:feature/5", + "id_v1": "don:DEV-0:feature:5", + "name": "Default Feature 5", + "type": "feature" + }, + "body": "Install the PLuG widget into your application with just a few lines of code and immediately bring the voice of your customer to your entire team. \n\nYou can also test the PLuG widget by clicking DevRev Org settings (top left DevRev icon) -> Support -> Try out PLuG.\n\nFollow step by step guide and copy and paste code from the link here -> https://devrev.ai/docs/plug/installation", + "created_by": { + "display_handle": "devrev-bot", + "display_id": "SYSU-1", + "display_name": "devrev-bot", + "full_name": "DevRev Bot", + "id": "don:identity:dvrv-us-1:devo/0:sysu/1", + "id_v1": "don:DEV-0:sys_user:SYSU-1", + "type": "sys_user" + }, + "created_date": "2023-01-31T12:04:25.946Z", + "custom_fields": null, + "display_id": "ISS-12", + "id": "don:core:dvrv-us-1:devo/0:issue/12", + "id_v1": "don:DEV-0:issue:12", + "links": [ + { + "link_id": "don:core:dvrv-us-1:devo/0:link/11", + "link_id_v1": "don:DEV-0:link:11", + "link_type": "is_dependency_of", + "target": "don:core:dvrv-us-1:devo/0:ticket/4", + "target_object_type": "ticket", + "target_v1": "don:DEV-0:ticket:4" + } + ], + "modified_by": { + "display_handle": "i-dev-user", + "display_id": "DEVU-2", + "display_name": "i-dev-user", + "email": "i-dev-user@devrev.ai", + "full_name": "Dev User", + "id": "don:identity:dvrv-us-1:devo/0:devu/2", + "id_v1": "don:DEV-0:dev_user:DEVU-2", + "state": "active", + "type": "dev_user" + }, + "modified_date": "2023-02-07T10:08:09.59Z", + "owned_by": [ + { + "display_handle": "devrev-bot", + "display_id": "SYSU-1", + "display_name": "devrev-bot", + "full_name": "DevRev Bot", + "id": "don:identity:dvrv-us-1:devo/0:sysu/1", + "id_v1": "don:DEV-0:sys_user:SYSU-1", + "type": "sys_user" + } + ], + "priority": "p1", + "stage": { + "name": "next", + "ordinal": 3000 + }, + "state": "open", + "stock_schema_fragment": "don:core:dvrv-us-1:stock_sf/292711", + "tags": [ + { + "id": { + "display_id": "TAG-1", + "id": "don:core:dvrv-us-1:devo/0:tag/1", + "id_v1": "don:DEV-0:tag:1", + "name": "" + }, + "tag": { + "display_id": "TAG-1", + "id": "don:core:dvrv-us-1:devo/0:tag/1", + "id_v1": "don:DEV-0:tag:1", + "name": "" + } + } + ], + "title": "'Install PLuG Today!' - Needs Dev Attention", + "type": "issue" + } + } + } + } +] +``` diff --git a/codelabs/10-vacation-responder.mdx b/codelabs/10-vacation-responder.mdx new file mode 100644 index 0000000..8572580 --- /dev/null +++ b/codelabs/10-vacation-responder.mdx @@ -0,0 +1,204 @@ +--- +title: 'Vacation Responder' +description: 'A snap-in that uses user-level settings to post a custom vacation message when an issue is assigned to a user who is on vacation.' +--- + +## Setup + +This section guides you on setting up the vacation responder snap-in. + +### Prerequisites + +- Node.js and `npm` installed. +- A DevRev account with the CLI installed and configured. + +### 1. Get the Code + +You can start by using the code in the `10-vacation-responder` directory. + +### 2. Configure the Snap-in + +This snap-in is configured through user-level settings. After a user installs the snap-in, they can go to their DevRev settings to configure it. They will see two options: +- **On Vacation:** A checkbox to enable or disable the responder. +- **Vacation Message:** A text field for their custom away message. + +## Manifest + +The `manifest.yaml` file defines the user-level inputs for the vacation settings. It also includes a JQ filter on the event source to ensure the automation only runs when an issue is created for or assigned to the user who has configured the snap-in. + +```yaml +version: "2" +name: "Vacation Responder" +description: "Respond with a custom message when on vacation" + +service_account: + display_name: Vacation Responder Bot + +inputs: + user: + - name: on_vacation + field_type: bool + ui: + display_name: On Vacation + + - name: vacation_message + description: Message to send when on vacation + field_type: text + ui: + display_name: Vacation message + +event_sources: + user: + - name: devrev-user-event-source + description: Event source per user listening on DevRev events. + display_name: DevRev user events listener + type: devrev-webhook + config: + event_types: + - work_updated + - work_created + filter: + jq_query: | + if .type == "work_created" then + if (.work_created.work.type == "issue" and .work_created.work.owned_by[0].id == $user.id) then true + else false + end + else + if (.work_updated.work.type == "issue" and .work_updated.work.owned_by[0].id == $user.id) then true + else false + end + end +functions: + - name: vacation_responder + description: Function to respond on vacation + +automations: + - name: vacation_responder_automation + source: devrev-user-event-source + event_types: + - work_created + - work_updated + function: vacation_responder +``` + +## Code + +The logic is in `10-vacation-responder/code/src/functions/vacation_responder/index.ts`. When triggered, it checks if the new owner of an issue has their "On Vacation" setting enabled. If so, it posts their custom vacation message to the issue's timeline. + +```typescript +/* + * Copyright (c) 2023 DevRev, Inc. All rights reserved. + */ + +import { client } from '@devrev/typescript-sdk'; +import { TimelineEntriesCreateRequestType } from '@devrev/typescript-sdk/dist/auto-generated/beta/beta-devrev-sdk'; +import { AxiosError } from 'axios'; + +function objectToMap(obj: { [key: string]: any }): Map { + const map = new Map(); + for (const key in obj) { + if (obj.hasOwnProperty(key)) { + map.set(key, obj[key]); + } + } + return map; +} + +function validateEvent(event: any): boolean { + if (event.payload.type === 'work_created') { + return true; + } else if (event.payload.type === 'work_updated') { + if (event.payload.work_updated.work.owned_by[0].id !== event.payload.work_updated.old_work.owned_by[0].id) { + return true; + } + } + return false; +} + +async function engine(event: any) { + const devrevPAT = event.context.secrets.service_account_token; + const apiBase = event.execution_metadata.devrev_endpoint; + const betaClient = client.setupBeta({ + endpoint: apiBase, + token: devrevPAT, + }); + const apiClient = client.setup({ + endpoint: apiBase, + token: devrevPAT, + }); + + if (!validateEvent(event)) return; + const eventType = event.payload.type; + const work = event.payload[eventType].work; + const workOwner = work.owned_by[0].id; + const snapInID = event.context.snap_in_id; + // get the creator's snap-in resources + try { + const userResourcesResponse = await betaClient.snapInsResources({ + id: snapInID, + user: workOwner, + }); + const userResourcesData = userResourcesResponse.data; + if (userResourcesData.inputs) { + const inputs = userResourcesData.inputs; + const inputsMap = objectToMap(inputs); + if (inputsMap.get('on_vacation') == true) { + const vacation_message = inputsMap.get('vacation_message') as string; + if (vacation_message && vacation_message.length > 0) { + await apiClient.timelineEntriesCreate({ + body: vacation_message, + type: TimelineEntriesCreateRequestType.TimelineComment, + object: work.id, + }); + console.log('Vacation message added to work item.'); + } + } else { + console.log("User isn't on vacation.", inputs); + } + } + } catch (error: any) { + // check if the error is an AxiosError + if (error.isAxiosError) { + const axiosError = error as AxiosError; + if (axiosError.response?.status === 404) { + console.log("User hasn't set up their snap-in resources yet."); + } else { + console.error('Error fetching user resources:', axiosError); + } + } + return; + } +} + +export const run = async (events: any[]) => { + for (const event of events) { + await engine(event); + } +}; + +export default run; +``` + +## Run + +You can test the `vacation_responder` function locally using the provided fixture, which simulates a `work_created` event. + +1. Navigate to the `10-vacation-responder/code` directory. +2. Install dependencies: + ```bash + npm install + ``` +3. Run the local test runner: + ```bash + npm run start:watch -- --functionName=vacation_responder --fixturePath=work_created.json + ``` + +## Verify + +When you run the function locally with the provided fixture, the function will attempt to fetch the vacation settings for the user who owns the new issue. Since the local test environment does not have any user settings configured, the API call will not find any and the function will log the following message to the console: + +``` +User hasn't set up their snap-in resources yet. +``` + +To fully test the logic, you need to install the snap-in in your DevRev organization, have a user configure their vacation message, and then assign an issue to that user. The vacation message should then appear on the issue's timeline. diff --git a/codelabs/11-hook-example.mdx b/codelabs/11-hook-example.mdx new file mode 100644 index 0000000..10e544e --- /dev/null +++ b/codelabs/11-hook-example.mdx @@ -0,0 +1,173 @@ +--- +title: 'Input Validation with Hooks' +description: 'A snap-in that demonstrates how to use validate hooks to ensure user inputs (such as account IDs and stages) are valid before they are saved.' +--- + +## Setup + +This section guides you on setting up the input validation snap-in. + +### Prerequisites + +- Node.js and `npm` installed. +- A DevRev account with the CLI installed and configured. + +### 1. Get the Code + +You can start by using the code in the `11-hook-example` directory. + +### 2. Configure the Snap-in + +This snap-in is configured through its input fields. The validation logic is triggered automatically when you attempt to save the configuration. The inputs are: +- **Account ID:** The ID of a DevRev account. +- **Initial Stage:** An issue stage. +- **Final Stage:** A different issue stage. + +## Manifest + +The `manifest.yaml` file defines the input fields for the snap-in and, most importantly, the `validate` hook. This hook points to the `validate_input` function, which contains the logic to check the inputs before they are saved. + +```yaml +version: '2' + +name: RevOrg Info +description: Gets information about a revorg from an account. + +service_account: + display_name: 'RevOrg Bot' + +inputs: + organization: + - name: account_id + description: The ID of the account. + field_type: text + is_required: true + default_value: 'don:identity:dvrv-us-1:devo/XXXXX:account/XXXXX' + ui: + display_name: Account ID + - name: initial_stage + description: The Initial Stage from which the stage is to be updated. + field_type: enum + allowed_values: + [ + 'Queued', + 'Awaiting Product Assist', + 'Awaiting Development', + 'In Development', + 'Work In Progress', + 'Awaiting Customer Response', + 'Resolved', + 'Canceled', + 'Accepted', + ] + default_value: 'Awaiting Customer Response' + ui: + display_name: Initial Stage + - name: final_stage + description: The Final Stage to which the stage is to be updated. + field_type: enum + allowed_values: + [ + 'Queued', + 'Awaiting Product Assist', + 'Awaiting Development', + 'In Development', + 'Work In Progress', + 'Awaiting Customer Response', + 'Resolved', + 'Canceled', + 'Accepted', + ] + default_value: 'Work In Progress' + ui: + display_name: Final Stage + +functions: + - name: validate_input + description: Function to validate the input. + +hooks: + - type: validate + function: validate_input +``` + +## Code + +The validation logic is in `11-hook-example/code/src/functions/validate_input/index.ts`. The function first checks if the initial and final stages are different. Then, it attempts to fetch the account using the provided `account_id` to validate its existence. If either check fails, it throws an error, which prevents the settings from being saved and displays the error message to the user. + +```typescript +import { client } from '@devrev/typescript-sdk'; + +// Validating the input by fetching the account details. +async function handleEvent(event: any) { + // Extract necessary information from the event + const token = event.context.secrets['service_account_token']; + const endpoint = event.execution_metadata.devrev_endpoint; + + // Set up the DevRev SDK with the extracted information + const devrevSDK = client.setupBeta({ + endpoint: endpoint, + token: token, + }); + + // Extract the part ID and commits from the event + const accountId = event.input_data.global_values['account_id']; + const initialStage = event.input_data.global_values['initial_stage']; + const finalStage = event.input_data.global_values['final_stage']; + + // Check the intitial and final stages are not equal + if (initialStage === finalStage) { + // eslint-disable-next-line @typescript-eslint/no-throw-literal + throw 'Initial and final stages cannot be the same. Please provide different stages.'; + } + + try { + // Create a timeline comment using the DevRev SDK + const response = await devrevSDK.accountsGet({ + id: accountId, + }); + console.log(JSON.stringify(response.data)); + // Return the response from the DevRev API + return response; + } catch (error) { + console.error(error); + // Handle the error here + // eslint-disable-next-line @typescript-eslint/no-throw-literal + throw 'Failed to fetch account details. Please provide the right account ID.'; + } +} + +export const run = async (events: any[]) => { + for (const event of events) { + await handleEvent(event); + } +}; + +export default run; +``` + +## Run + +You can test the `validate_input` function locally using the provided fixture. + +1. Navigate to the `11-hook-example/code` directory. +2. Install dependencies: + ```bash + npm install + ``` +3. Run the local test runner: + ```bash + npm run start:watch -- --functionName=validate_input --fixturePath=event.json + ``` + +## Verify + +When you run the function locally with the provided fixture, it will attempt to validate the inputs. The provided `account_id` is a placeholder, so the `accountsGet` API call will fail. This is expected behavior for the local test. + +The function will catch this API error and throw a new error. You should see an error message in your console similar to this: + +``` +Failed to fetch account details. Please provide the right account ID. +``` + +This confirms that the validation hook's error path is working correctly. To test the success path, you would need to replace the placeholder `account_id` in `event.json` with a valid account ID from your DevRev organization. diff --git a/codelabs/12-csat.mdx b/codelabs/12-csat.mdx new file mode 100644 index 0000000..80c3ab2 --- /dev/null +++ b/codelabs/12-csat.mdx @@ -0,0 +1,361 @@ +--- +title: 'CSAT Surveys' +description: 'A snap-in that creates and processes Customer Satisfaction (CSAT) surveys in DevRev, with automations for conversation closure and a slash command for on-demand surveys.' +--- + +## Setup + +This section guides you on setting up the CSAT survey snap-in. + +### Prerequisites + +- Node.js and `npm` installed. +- A DevRev account with the CLI installed and configured. + +### 1. Get the Code + +You can start by using the code in the `12-csat` directory. + +### 2. Configure the Snap-in + +This snap-in can be customized using the global inputs defined in the manifest files. You can set the survey channels, the text for headers and questions, the response scale, and the survey expiration time. + +This example comes with two manifest files: one for conversations (`manifest_conv.yaml`) and one for tickets (`manifest_tkt.yaml`). You will need to choose which one to deploy. + +## Manifests + +This snap-in includes two manifest files to handle surveys for conversations and tickets separately. + +
+manifest_conv.yaml (for Conversations) + +```yaml +version: "1" + +name: "CSAT on Conversation" +description: "Capture the satisfaction level for customer conversations on PLuG to enhance the customer experience." + +service_account: + display_name: "DevRev Bot" + +event-sources: + - name: devrev-webhook + description: Event coming from DevRev + display_name: DevRev + type: devrev-webhook + config: + event_types: + - conversation_updated + +globals: + - name: survey_channel + description: The channel the survey is sent on. + devrev_field_type: '[]enum' + devrev_enum: ["PLuG", "Email"] + default_value: ["PLuG", "Email"] + ui: + display_name: Survey channel + - name: survey_text_header + description: Introductory text posted on timeline when survey is populated. + devrev_field_type: text + default_value: "We would love to hear your feedback." + ui: + display_name: Survey introductory text + - name: survey_resp_scale + description: Response values to be displayed on the survey scale (high to low). + devrev_field_type: text + default_value: "Great,Good,Average,Poor,Awful" + ui: + display_name: Survey response scale + - name: survey_text + description: Text posted on timeline when survey is populated. + devrev_field_type: text + default_value: "How satisfied were you with this chat?" + ui: + display_name: Survey query + - name: survey_resp_text + description: Text posted on timeline when survey response is submitted. + devrev_field_type: text + default_value: "Thank you for sharing your valuable feedback with us! Your insights are greatly appreciated." + ui: + display_name: Survey response message + - name: survey_expires_after + description: "Indicates the time (in minutes) for which the survey remains active (minimum 1 minute)" + devrev_field_type: int + default_value: 1440 + ui: + display_name: Survey expires after + +functions: + - name: post_survey + description: Create a survey comment on conversation closure. + - name: process_response + description: Process survey response for conversation survey response. + +commands: + - name: survey + namespace: csat_on_conversation + description: Capture the customer satisfaction level with ongoing interaction. + surfaces: + - surface: discussions + object_types: + - conversation + usage_hint: "[chat/email] [survey question]" + function: post_survey + +automations: + - name: Add survey as a comment on resolved object + source: devrev-webhook + event_types: + - conversation_updated + function: post_survey + +snap_kit_actions: + - name: survey + description: Snap kit action for processing `survey` response + function: process_response +``` +
+ +
+manifest_tkt.yaml (for Tickets) + +```yaml +version: "1" + +name: "CSAT on Ticket" +description: "Capture the satisfaction level for customer tickets on support portal to enhance the customer experience." + +service_account: + display_name: "DevRev Bot" + +event-sources: + - name: devrev-webhook + description: Event coming from DevRev + display_name: DevRev + type: devrev-webhook + config: + event_types: + - work_updated + +globals: + - name: survey_channel + description: The channel the survey is sent on. + devrev_field_type: '[]enum' + devrev_enum: ["Portal", "Email"] + default_value: ["Portal", "Email"] + ui: + display_name: Survey channel + - name: survey_text_header + description: Introductory text posted when survey is populated. + devrev_field_type: text + default_value: "We would love to hear your feedback." + ui: + display_name: Survey introductory text + - name: survey_resp_scale + description: Response values to be displayed on the survey scale (high to low). + devrev_field_type: text + default_value: "Great,Good,Average,Poor,Awful" + ui: + display_name: Survey response scale + - name: survey_text + description: Text posted when survey is populated. + devrev_field_type: text + default_value: "How satisfied were you with the support experience?" + ui: + display_name: Survey query + - name: survey_resp_text + description: Text posted when survey response is submitted. + devrev_field_type: text + default_value: "Thank you for sharing your valuable feedback with us! Your insights are greatly appreciated." + ui: + display_name: Survey response message + - name: survey_expires_after + description: "Indicates the time (in minutes) for which the survey remains active (minimum 1 minute)" + devrev_field_type: int + default_value: 1440 + ui: + display_name: Survey expires after + +functions: + - name: post_survey + description: Create a survey comment on ticket closure. + - name: process_response + description: Process survey response on ticket survey response. + +commands: + - name: survey + namespace: csat_on_ticket + description: Capture the customer satisfaction level with ongoing interaction. + surfaces: + - surface: discussions + object_types: + - ticket + usage_hint: "[chat/email] [survey question]" + function: post_survey + +automations: + - name: Add survey as a comment on resolved object + source: devrev-webhook + event_types: + - work_updated + function: post_survey + +snap_kit_actions: + - name: survey + description: Snap kit action for processing `survey` response + function: process_response +``` +
+ +## Code + +This snap-in has two main functions to separate the logic for posting a survey and processing the response. + +### `post_survey` + +This function is triggered when a conversation is closed (automation) or when a user runs the `/survey` command. It creates and posts an interactive Snap Kit card with the survey question. + +```typescript +// Located at 12-csat/code/src/functions/post_survey/index.ts +import { + doDevRevPostAPICall, + getAPIBase, + getSnapKitBody, + getTimelineCommentBody, + getAPIDomain, + getExpiryTimestamp, + getSurveyId, + getWork, + getConversation, + getCommandParameters, +} from '../common/utils'; +import { + EMAIL, + PLUG, + PORTAL, + CHAT, + INTERNAL, + PRIVATE, + TimelineEntriesCreateAPIMethodPath, + DefaultCSATName, + TimelineLabelDisplayCustomerChat, +} from '../common/constants'; + +interface Stakeholder { + id?: string; + email_id?: string; +} + +const commentExpireAt2Min = getExpiryTimestamp(2).toISOString(); + +export class PostSurvey { + constructor() {} + + async PostSurvey(event: any) { + console.log('Creating survey on event payload: ', JSON.stringify(event.payload)); + console.log('Event Context: ', JSON.stringify(event.context)); + try { + // ... (Full function implementation) ... + } catch (error) { + console.error('Error: ', error); + } + } + + getStakeholders(stakeholdersFromObj: any[]): Stakeholder[] { + // ... (Full function implementation) ... + } +} + +export const run = async (events: any[]) => { + console.log('Running SnapIn for survey - post_survey', events); + const postSurvey = new PostSurvey(); + for (let event of events) { + await postSurvey.PostSurvey(event); + } + console.info('events', events); +}; + +export default run; +``` + +### `process_response` + +This function is triggered when a user clicks a rating on the Snap Kit survey card. It submits the survey response, deletes the card, and posts a "thank you" message. + +```typescript +// Located at 12-csat/code/src/functions/process_response/index.ts +import { + doDevRevGetAPICall, + doDevRevPostAPICall, + getAPIBase, getSurveyId, + getTimelineCommentBody, + getExpiryTimestamp, +} from '../common/utils'; +import { + EMAIL, PLUG, PORTAL, DefaultCSATName, INTERNAL, PRIVATE, + TimelineEntriesCreateAPIMethodPath, + TimelineEntriesDeleteAPIMethodPath, + SurveysSubmitAPIMethodPath, + RevUsersGetAPIMethodPath, TimelineLabelDisplayCustomerChat, +} from '../common/constants'; + +async function ProcessSurveyResponse(event: any) { + // ... (Full function implementation) ... +} + +function getDeleteTimelineEntryBody(entryId: string) { + // ... (Full function implementation) ... +} + +function getSurveyResponseBody(surveyId: string, objId: string, rating: number, sourceChannel: string) { + // ... (Full function implementation) ... +} + +function getSourceChannel(payload: any) { + // ... (Full function implementation) ... +} + +export const run = async (events: any[]) => { + console.log('Running SnapIn for processing survey response'); + for (let event of events) { + await ProcessSurveyResponse(event); + } +}; + +export default run; +``` + +## Run + +You can test the `post_survey` function's automation trigger locally using the provided fixture. + +1. Navigate to the `12-csat/code` directory. +2. Install dependencies: + ```bash + npm install + ``` +3. Run the local test runner for the `post_survey` function: + ```bash + npm run start:watch -- --functionName=post_survey --fixturePath=conversation_updated_event.json + ``` + +> **Note:** There is no provided fixture to test the `process_response` function or the `/survey` slash command locally. These flows must be tested after deploying the snap-in to your DevRev organization. + +## Verify + +### `post_survey` Verification + +When you run the `post_survey` function locally with the provided fixture, the function will log the event payload and context, and then attempt to post the survey card. You should see logs like: +``` +Running SnapIn for survey - post_survey [...] +Creating survey on event payload: {"conversation_updated":{...}} +Event Context: {"dev_oid":...} +``` + +### `process_response` Verification + +This function can only be tested in a live environment. After a survey card is posted, click one of the rating buttons. You should observe the following: +1. The survey card is removed from the timeline. +2. A "thank you" message is posted in its place. +3. An internal note is added to the timeline with the rating that was selected. diff --git a/codelabs/13-keyring-type.mdx b/codelabs/13-keyring-type.mdx new file mode 100644 index 0000000..c0ed4eb --- /dev/null +++ b/codelabs/13-keyring-type.mdx @@ -0,0 +1,215 @@ +--- +title: 'Custom Keyring Types' +description: 'A guide to creating custom keyring types in a snap-in manifest, with examples for basic authentication, OAuth 2.0, multi-field secrets, and referencing existing types.' +--- + +## Introduction + +This guide demonstrates how to create custom keyring types to connect to third-party services. Custom keyring types allow you to define your own connection types, including support for different authentication methods and custom UI. This example includes four different types of custom keyrings: +- Basic authentication +- OAuth 2.0 +- Multi-field secrets +- Referencing existing keyring types + +### Prerequisites +- A DevRev account with the CLI installed and configured. + +## Setup + +This section guides you on setting up a new snap-in project to use custom keyring types. + +1. **Initialize the project:** Run `devrev snap_in_version init ` to create a new project directory with a template structure. +2. **Update the manifest:** Modify the `manifest.yaml` file to include your `keyring_types` definition, using the examples below as a reference. +3. **Validate the manifest:** Before deploying, check your manifest by running `devrev snap_in_version validate-manifest manifest.yaml`. + +## Keyring Type Examples + +Below are four examples of how to define custom keyring types in your `manifest.yaml`. + +### Basic Authentication + +This example shows how to create a custom keyring type for a service that uses basic authentication, such as Freshdesk. The manifest defines the required fields and how to transform them into a standard `Authorization: Basic` header. + +```yaml +# Example: custom-keyring-type-basic.yaml +version: "2" +name: "Custom Keyring Type Snap-in" +description: "Creating custom keyring type for Freshdesk Basic connection" + +keyrings: + organization: + - name: freshdesk_connection + display_name: Freshdesk connection (must be set up as dev org connection) + description: The Freshdesk app connection for the organization. + types: + - freshdesk-basic-connection + +keyring_types: + - id: freshdesk-basic-connection + name: Freshdesk Connection + description: Freshdesk connection + kind: "Secret" + is_subdomain: true # The is_subdomain field is used to indicate that the subdomain is part of the URL. + secret_config: # The secret_config section is used to define the fields in the secret. + secret_transform: ".token+\":X\" | @base64" # The secret_config section is used to transform data from the input fields into the secret value (token). + fields: # optional: data that the user shall provide in the input form when creating the connection. Each element represents one input field. Fields will be included in the final JSON secret. If omitted, the user will be asked for a generic secret. + - id: token + name: Token + description: Freshdesk API token + token_verification: # The token_verification section is used to verify the token provided by the user. + url: "https://[SUBDOMAIN].freshdesk.com/api/v2/tickets" + method: "GET" + headers: + Authorization: "Basic [API_KEY]" +``` + +### OAuth 2.0 + +This example shows how to create a custom keyring type for a service that uses OAuth 2.0, such as GitLab. It defines the necessary URLs, scopes, and secrets for the OAuth flow. + +```yaml +# Example: custom-keyring-type-oauth.yaml +version: "2" +name: "Custom Keyring Type Snap-in" +description: "Creating custom keyring type for GitLab OAuth connection" + +service_account: + display_name: DevRev Bot + +developer_keyrings: + - name: gitlab-oauth-secret + description: GitLab OAuth secret + display_name: GitLab OAuth secret + +keyrings: + organization: + - name: gitlab_connection + display_name: GitLab connection (must be set up as dev org connection) + description: The gitlab app connection for the organization. + types: + - gitlab-oauth-connection + +keyring_types: + - id: gitlab-oauth-connection + name: "GitLab Connection" + description: "GitLab connection" + kind: "Oauth2" + scopes: + - name: read + description: Read access + value: "read_api" + - name: api + description: API access + value: "api" + scope_delimiter: " " + oauth_secret: gitlab-oauth-secret + authorize: + type: "config" + auth_url: "https://gitlab.com/oauth/authorize" + token_url: "https://gitlab.com/oauth/token" + grant_type: "authorization_code" + auth_query_parameters: + "client_id": "[CLIENT_ID]" + "scope": "[SCOPES]" + "response_type": "code" + token_query_parameters: + "client_id": "[CLIENT_ID]" + "client_secret": "[CLIENT_SECRET]" + refresh: + type: "config" + url: "https://gitlab.com/api/oauth.v2.access" + method: "POST" + query_parameters: + "client_id": "[CLIENT_ID]" + "client_secret": "[CLIENT_SECRET]" + "refresh_token": "[REFRESH_TOKEN]" + headers: + "Content-type": "application/x-www-form-urlencoded" + revoke: + type: "config" + url: "https://gitlab.com/oauth/revoke" + method: "POST" + headers: + "Content-type": "application/x-www-form-urlencoded" + query_parameters: + "client_id": "[CLIENT_ID]" + "client_secret": "[CLIENT_SECRET]" + "token": "[ACCESS_TOKEN]" +``` + +### Multi-field Secrets + +This example shows how to create a custom keyring type for a secret that has multiple fields, such as a username and password. + +```yaml +# Example: custom-keyring-type-secret.yaml +version: "2" +name: "Custom Keyring Type Snap-in" +description: "Creating custom keyring type for Multi Field Secret" + +keyrings: + organization: + - name: multi_field_secret + display_name: Multi Field Secret + description: The multi field secret for the organization. + types: + - multi-field-secret + +keyring_types: + - id: multi-field-secret + name: Multi Field Secret + description: Multi Field Secret + kind: "Secret" + secret_config: + fields: + - id: username + name: Username + description: Username + - id: password + name: Password + description: Password + is_optional: true +``` + +### Referencing Existing Keyring Types + +This example shows how to create a custom keyring type that references an existing, built-in keyring type. This is useful for extending existing connection types with additional scopes or settings. + +```yaml +# Example: reference-keyring-type.yaml +version: "2" +name: "Reference Keyring Type Snap-in" +description: "Creating the keyring type for Slack connection with reference to the existing Slack connection" + +service_account: + display_name: DevRev Bot + +developer_keyrings: + - name: slack-oauth-secret + description: Slack OAuth secret + display_name: Slack OAuth secret + +keyrings: + organization: + - name: slack_connection + display_name: Slack connection (must be set up as dev org connection) + description: The slack app connection for the organization. + types: + - slack-oauth-connection + +keyring_types: + - id: slack-oauth-connection + name: Slack Connection + description: Slack connection + kind: "Oauth2" + scopes: + - name: read + description: App mentions read only access + value: app_mentions:read + - name: write + description: App channels history read only access + value: "channels:history" + scope_delimiter: "," + oauth_secret: slack-oauth-secret + reference_keyring: devrev-slack-oauth +``` diff --git a/codelabs/14-operations.mdx b/codelabs/14-operations.mdx new file mode 100644 index 0000000..3be8c64 --- /dev/null +++ b/codelabs/14-operations.mdx @@ -0,0 +1,241 @@ +--- +title: 'Custom Operations' +description: 'A guide to creating custom operations for the DevRev Workflow Builder, with examples for getting data, posting comments, and sending Slack messages.' +--- + +## Introduction + +This guide demonstrates how to create custom operations for the DevRev Workflow Builder. Custom operations are reusable nodes that can simplify and enhance your workflows by encapsulating specific logic. This Codelab provides examples for three custom operations: +- **Get Temperature**: A simple operation that returns a value based on an input. +- **Post Comment on Ticket**: An operation that uses the DevRev SDK to interact with DevRev objects. +- **Send Slack Message**: An operation that connects to an external system (Slack) to send a message. + +### Prerequisites +- A DevRev account with the CLI installed and configured. +- A Slack workspace and a bot token (required for the "Send Slack Message" operation). + +## Setup + +This section guides you on setting up a new snap-in project to house your custom operations. + +1. **Initialize the project:** Run `devrev snap_in_version init ` to create a new project directory. +2. **Update the manifest:** Modify the `manifest.yaml` file to include your `operations` definitions, using the examples below as a reference. You can define multiple operations in a single manifest. +3. **Validate the manifest:** Before deploying, check your manifest by running `devrev snap_in_version validate-manifest manifest.yaml`. + +## How Custom Operations Work + +Custom operations are implemented as classes that extend the `OperationBase` class from the DevRev TypeScript SDK. A single function, typically named `operation_handler`, acts as a dispatcher. It receives an event, identifies which operation was triggered based on its `slug`, and instantiates the corresponding class to execute the logic. + +Here is the dispatcher code from `14-operations/code/src/functions/operation_handler/index.ts`: + +```typescript +import { OperationFactory } from '../../operations'; +import { ExecuteOperationInput,FunctionInput, OperationMap } from '@devrev/typescript-sdk/dist/snap-ins'; + +// Operations +import { GetTemperature } from './get_temperature'; +import { PostCommentOnTicket } from './post_comment_on_ticket'; +import { SendSlackMessage } from './send_slack_message'; + +/** + * Map of operations with the slug mentioned in the manifest. + * The key is the slug of the operation mentioned in the manifest and value is the operation class. + */ +const operationMap: OperationMap = { + get_temperature: GetTemperature, + post_comment_on_ticket: PostCommentOnTicket, + send_slack_message: SendSlackMessage, +}; + +export const run = async (events: FunctionInput[]) => { + const event = events[0]; + const payload = event.payload as ExecuteOperationInput + const operationSlug = payload.metadata!.slug; + const operationNamespace = payload.metadata!.namespace; + console.log('running operation: ', operationSlug, ' in namespace: ', operationNamespace); + const operationFactory = new OperationFactory(operationMap); + const operation = operationFactory.getOperation(operationSlug, event); + const ctx = operation.GetContext(event); + const resources = event.input_data.resources||{}; + return await operation.run(ctx, payload, resources); +}; + +export default run; +``` + +## Operation Examples + +Below are three examples of custom operations, each with its manifest definition and corresponding code. + +### 1. Get Temperature + +This simple operation takes a city as input and returns a hardcoded temperature value. It demonstrates the basic structure of an operation with inputs and outputs. + +**Manifest Snippet** +```yaml + - name: get_temperature + display_name: Get Temperature + description: Operation to get the temperature of a city + slug: get_temperature + function: operation_handler + type: action + inputs: + fields: + - name: city + field_type: enum + allowed_values: + - New York + - San Francisco + - Los Angeles + - Chicago + - Houston + is_required: true + default_value: "New York" + ui: + display_name: City + outputs: + fields: + - name: temperature + field_type: double + ui: + display_name: Temperature +``` + +**Code (`get_temperature.ts`)** +```typescript +import { Error as OperationError, Error_Type, ExecuteOperationInput, FunctionInput, OperationBase, OperationContext, OperationOutput, OutputValue } from '@devrev/typescript-sdk/dist/snap-ins'; + +interface GetTemperatureInput { city: string; } + +export class GetTemperature extends OperationBase { + constructor(e: FunctionInput) { super(e); } + + override GetContext(): OperationContext { + let baseMetadata = super.GetContext(); + const temperatures: Record = { + 'New York': 72, 'San Francisco': 65, 'Seattle': 55, + 'Los Angeles': 80, 'Chicago': 70, 'Houston': 90, + }; + return { ...baseMetadata, metadata: temperatures }; + } + + async run(_context: OperationContext, input: ExecuteOperationInput, _resources: any): Promise { + const input_data = input.data as GetTemperatureInput; + const temperature = _context.metadata ? _context.metadata[input_data.city] : null; + let err: OperationError | undefined = undefined; + if (!temperature) { + err = { message: 'City not found', type: Error_Type.InvalidRequest }; + } + const temp = { error: err, output: { values: [{ "temperature": temperature }] } as OutputValue }; + return OperationOutput.fromJSON(temp); + } +} +``` + +### 2. Post Comment on Ticket + +This operation uses the DevRev SDK to post a comment to a ticket, demonstrating how to interact with DevRev objects. + +**Manifest Snippet** +```yaml + - name: post_comment_on_ticket + display_name: Post Comment on Ticket + description: Operation to post a comment on ticket + slug: post_comment_on_ticket + function: operation_handler + type: action + inputs: + fields: + - name: id + description: Ticket ID to post comment on. + field_type: text + is_required: true + ui: + display_name: Ticket ID + - name: comment + description: Comment to post on ticket. + field_type: text + is_required: true + ui: + display_name: Comment + outputs: + fields: + - name: comment_id + field_type: text + ui: + display_name: Comment ID +``` + +**Code (`post_comment_on_ticket.ts`)** +```typescript +import { client } from '@devrev/typescript-sdk'; +import { TimelineEntriesCreateRequestType } from '@devrev/typescript-sdk/dist/auto-generated/beta/beta-devrev-sdk'; +import { Error as OperationError, Error_Type, ExecuteOperationInput, FunctionInput, OperationBase, OperationContext, OperationOutput, OutputValue } from '@devrev/typescript-sdk/dist/snap-ins'; + +interface PostCommentOnTicketInput { id: string; comment: string; } + +export class PostCommentOnTicket extends OperationBase { + constructor(e: FunctionInput) { super(e); } + + async run(context: OperationContext, input: ExecuteOperationInput, _resources: any): Promise { + // ... (Full function implementation) ... + } +} +``` + +### 3. Send Slack Message + +This operation connects to Slack to send a message, demonstrating how to integrate with external systems and use keyrings for authentication. + +**Manifest Snippet** +```yaml + - name: send_slack_message + display_name: Send Slack Message + description: Operation to send a message to a Slack channel/thread + slug: send_slack_message + function: operation_handler + type: action + keyrings: + - name: slack_token + display_name: Slack Connection + description: Connection to Slack + types: + - slack + inputs: + fields: + - name: channel + description: Channel to send message to. + field_type: text + is_required: true + ui: + display_name: Channel + - name: message + description: Message to send. + field_type: rich_text + is_required: true + ui: + display_name: Message + outputs: + fields: + - name: message_id + field_type: text + ui: + display_name: Message ID +``` + +**Code (`send_slack_message.ts`)** +```typescript +import { Error as OperationError, Error_Type, ExecuteOperationInput, FunctionInput, OperationBase, OperationContext, OperationOutput, OutputValue } from '@devrev/typescript-sdk/dist/snap-ins'; +import { WebClient } from '@slack/web-api'; + +interface SendSlackMessageInput { channel: string; message: string; } + +export class SendSlackMessage extends OperationBase { + constructor(e: FunctionInput) { super(e); } + async run(context: OperationContext, input: ExecuteOperationInput, resources: any): Promise { + // ... (Full function implementation) ... + } +} +``` + +> **Note:** Custom operations are designed to be used as nodes within the DevRev Workflow Builder. They are not meant to be run directly. To test them, you must create a workflow that uses your custom operation and then trigger that workflow. diff --git a/codelabs/15-adaas.mdx b/codelabs/15-adaas.mdx new file mode 100644 index 0000000..b05fbf7 --- /dev/null +++ b/codelabs/15-adaas.mdx @@ -0,0 +1,46 @@ +--- +title: 'Automation as a Service (AdaaS)' +description: 'A template for an "Automation as a Service" (AdaaS) snap-in. The implementation for this Codelab is pending.' +--- + +> **Note:** This Codelab is a placeholder. The associated snap-in and its code are pending implementation. The content below serves as a template for what the Codelab will look like once the example is built. + +## Overview + +This document outlines a future "Automation as a Service" (AdaaS) snap-in. The `15-adaas/code` directory for this example is currently empty. + +### Prerequisites +- Node.js and `npm` installed. +- A DevRev account. +- The DevRev CLI installed and configured. + +## Setup + +This section describes the general steps for setting up a new snap-in project. + +### Initializing a New Project +To create a new snap-in, you would use the DevRev CLI. + +1. **Initialize the project:** Run `devrev snap_in_version init ` to create a new project directory with a template structure. +2. **Validate the manifest:** Once the manifest is created, check it by running `devrev snap_in_version validate-manifest manifest.yaml`. +3. **Prepare test data:** For local testing, you would create a JSON file in `code/src/fixtures/` with a sample event payload. + +## Manifest + +A `manifest.yaml` file will be created in the `15-adaas/` directory to define the snap-in's properties, functions, and other configurations. + +## Code + +The core functions for the AdaaS snap-in will be implemented in the `code/src/functions` directory. + +## Run + +Local testing procedures will be defined here once the snap-in is implemented. + +## Verify + +Verification steps for the implemented features will be added to this section. + +## Explanation + +An explanation of the AdaaS snap-in's functionality will be provided once it is implemented. diff --git a/codelabs/2-notify-owner-on-ticket-to-prod-assist.mdx b/codelabs/2-notify-owner-on-ticket-to-prod-assist.mdx new file mode 100644 index 0000000..7541161 --- /dev/null +++ b/codelabs/2-notify-owner-on-ticket-to-prod-assist.mdx @@ -0,0 +1,155 @@ +--- +title: 'Notify Owner on Ticket to Prod Assist' +description: 'An automation that posts a comment on a ticket when its stage changes to "Awaiting Product Assist" to notify the part owner.' +--- + +## Setup + +This section guides you on setting up a new snap-in project and explains the structure of this example. + +### Prerequisites + +- Node.js and `npm` installed. +- A DevRev account with the CLI installed and configured. + +### 1. Initialize Your Project + +To create a new snap-in, run the following command in your terminal, replacing `` with your desired project name: + +```bash +devrev snap_in_version init +``` + +### 2. Validate the Manifest + +Before writing any code, validate the template's `manifest.yaml` file by running the following command from your project's root directory: + +```bash +devrev snap_in_version validate-manifest manifest.yaml +``` + +### 3. Prepare Test Data + +For local testing, you need a sample event payload. This example includes a fixture file at `code/src/fixtures/status_change.json`. + +## Manifest + +The `manifest.yaml` file defines the event source, the function, and the automation that ties them together. This configuration listens for `work_updated` events and triggers the `ticket_stage_change` function in response. + +```yaml +version: "2" +name: "Notify On Prod Assist" +description: "Snap-In to post a comment on a ticket when its stage changes to 'Awaiting Product Assist'" + +service_account: + display_name: "DevRev Bot" + +event_sources: + organization: + - name: devrev-webhook + description: Source listening for work_updated events from DevRev. + display_name: DevRev Webhook + type: devrev-webhook + config: + event_types: + - work_updated + +functions: + - name: ticket_stage_change + description: Function to post a comment on a ticket when its stage changes to "Awaiting Product Assist". + +automations: + - name: add_comment_on_ticket_stage_change + source: devrev-webhook + event_types: + - work_updated + function: ticket_stage_change +``` + +## Code + +The core logic is in `2-notify-owner-on-ticket-to-prod-assist/code/src/functions/ticket_stage_change/index.ts`. It checks if a ticket has been moved to the "awaiting_product_assist" stage and, if so, posts a comment to the ticket timeline to notify the relevant part owner. + +```typescript +/* + * Copyright (c) 2023 DevRev, Inc. All rights reserved. + */ + +import { + getPart, + getPartOwnersString, + ticketTimelineEntryCreate, +} from "./utils/devrev-utils" +import { + sprintf +} from 'sprintf-js'; + +// Timeline Comment if the part owner of a ticket is devrev-bot +const BOT_PART_OWNER_NOTIF: string = `Hey, this ticket moved to Product Assist stage and may need attention.`; +const PART_OWNER_NOTIF: string = `Hey %s, this ticket moved to Product Assist stage and may need your attention. You are being notified because you are the part owner of this ticket.`; + +async function EventListener(event: any) { + const oldStage: string = event.payload.work_updated.old_work.stage.name; + const currStage: string = event.payload.work_updated.work.stage.name; + const workType: string = event.payload.work_updated.work.type; + const snap_in_token = event.context.secrets.service_account_token; + try { + if (!( + currStage === "awaiting_product_assist" && + oldStage !== "awaiting_product_assist" && + workType === "ticket" + )) return; + + const ticketID = event.payload.work_updated.work.id; + const partID = event.payload.work_updated.work.applies_to_part.id; + const partObject = await getPart(partID, snap_in_token); + + console.log(`Ticket ${ticketID} moved to Product Assist stage`); + + if ((partObject.part.owned_by).length == 1 && partObject.part.owned_by[0].type != "dev_user") { + console.log("A bot is the part owner"); + await ticketTimelineEntryCreate(ticketID, BOT_PART_OWNER_NOTIF, snap_in_token); + } else { + let partOwners = await getPartOwnersString(partObject); + if (partOwners != "") { + console.log("Creating timeline entry for the part owners"); + await ticketTimelineEntryCreate(ticketID, sprintf(PART_OWNER_NOTIF, [partOwners]), snap_in_token); + } else + console.log("No part owners to notify regarding the stage change"); + } + } catch (error) { + console.error('Error: ', error); + } +} + +export const run = async (events: any[]) => { + for (let i = 0; i < events.length; i++) { + await EventListener(events[i]); + } +}; +export default run; +``` + +## Run + +To run the function locally, navigate to the `2-notify-owner-on-ticket-to-prod-assist/code` directory and execute the following commands. + +1. **Install dependencies:** + ```bash + npm install + ``` + +2. **Run the local test runner:** + ```bash + npm run start:watch -- --functionName=ticket_stage_change --fixturePath=status_change.json + ``` + +## Verify + +After running the local test runner with the provided fixture, you should see the following output in your console. This confirms that the function correctly identified the stage change. + +``` +Ticket don:core:dvrv-us-1:devo/test-org:ticket/126 moved to Product Assist stage +``` + +In a live environment, this would be followed by a timeline comment being posted to the ticket. Due to dependencies on API calls, further output cannot be reliably determined in a local test run. diff --git a/codelabs/3-giphy-template.mdx b/codelabs/3-giphy-template.mdx new file mode 100644 index 0000000..5e2dd9b --- /dev/null +++ b/codelabs/3-giphy-template.mdx @@ -0,0 +1,281 @@ +--- +title: 'Giphy Snap-in Template' +description: 'A snap-in that allows users to search for and post GIFs in discussions using a slash command and automatically posts a celebratory GIF when an issue is closed.' +--- + +## Setup + +This section guides you on setting up the Giphy snap-in. + +### Prerequisites + +- Node.js and `npm` installed. +- A DevRev account with the CLI installed and configured. +- A Giphy API key from the [Giphy Developers](https://developers.giphy.com/) website. + +### 1. Get the Code + +Since this is a template, you can start by using the code in the `3-giphy-template` directory. + +### 2. Configure the Snap-in + +This snap-in requires a Giphy API key to function. You will need to provide this key when you install the snap-in in your DevRev organization. The key is defined as an input field in the manifest. + +## Manifest + +The `manifest.yaml` file defines the necessary inputs, commands, and automations for the snap-in to work. + +```yaml +version: "2" +name: "Giphy Snap-in" +description: "A snap-in to search and post gifs on the DevRev Timeline" + +service_account: + display_name: Giphy Bot + +event_sources: + organization: + - name: devrev-webhook + description: Event coming from DevRev + display_name: Devrev + type: devrev-webhook + config: + event_types: + - work_updated + +inputs: + organization: + - name: giphy_api_key + description: Giphy API key + field_type: text + +functions: + - name: search_giphy + description: Search a gif with given tag on giphy.com + - name: render_giphy + description: Render a given gif + - name: publish_giphy_on_work_closed + description: Published giphy + +commands: + - name: giphy + namespace: devrev + description: Create a new gif + surfaces: + - surface: discussions + object_types: + - issue + - ticket + - conversation + - part + - rev_user + - rev_org + usage_hint: "[text]" + function: search_giphy + +snap_kit_actions: + - name: giphy + description: Snap kit action for showing gif created using `giphy` command + function: render_giphy + +automations: + - name: Add giphy when issue closed + source: devrev-webhook + event_types: + - work_updated + function: publish_giphy_on_work_closed +``` + +## Code + +The snap-in has multiple functions. The core logic for the slash command is in `3-giphy-template/code/src/functions/search_giphy/index.ts`. It's triggered by `/giphy [search term]` and fetches a random GIF from Giphy. + +```typescript +/* + * Copyright (c) 2023 DevRev, Inc. All rights reserved. + */ + +import fetch from 'node-fetch'; + + +async function CreateGiphySnapKit(input: any, imagesMeta: any) { + console.log('Creating snap kit to render fetched gif'); + const url = 'https://api.devrev.ai/timeline-entries.create'; + try { + const resp = await fetch(url, { + method: 'POST', + headers: { + 'authorization': input.context.secrets['service_account_token'], + 'content-type': 'application/json', + 'accept': 'application/json, text/plain, */*', + }, + body: JSON.stringify({ + 'object': input.payload.source_id, + 'body': 'Giphy', + 'type': 'timeline_comment', + 'snap_kit_body': { + 'snap_in_id': input.context.snap_in_id, + 'snap_in_action_name': 'giphy', + 'body': { + 'snaps': [{ + 'type': 'card', + 'title': { + 'text': input.payload.parameters, + 'type': 'plain_text', + }, + 'elements': [ + { + 'elements': [ + { + 'alt_text': 'Awesome GIF', + 'image_url': imagesMeta.fixed_width_downsampled.url, + 'block_id': imagesMeta.downsized.url, + 'type': 'image', + }, + ], + 'type': 'content', + }, + { + 'direction': 'row', + 'justify': 'center', + 'type': 'actions', + 'elements': [ + { + 'action_id': 'send', + 'action_type': 'remote', + 'style': 'primary', + 'type': 'button', + 'value': 'send', + 'text': { + 'text': 'Send', + 'type': 'plain_text', + }, + }, + { + 'action_id': 'shuffle', + 'action_type': 'remote', + 'style': 'primary', + 'type': 'button', + 'value': 'shuffle', + 'text': { + 'text': 'Shuffle', + 'type': 'plain_text', + }, + }, + { + 'action_id': 'cancel', + 'action_type': 'remote', + 'style': 'danger', + 'type': 'button', + 'value': 'cancel', + 'text': { + 'text': 'Cancel', + 'type': 'plain_text', + }, + }, + ], + }, + ], + }], + }, + }, + }), + }); + + if (resp.ok) { + console.log('Giphy snap kit created successfully'); + } else { + let body = await resp.text(); + console.log('Error while posting to timeline: ', resp.status, body); + } + } catch (error) { + console.log('Failed to post to timeline: ', error); + } +} + +export const run = async (events: any[]) => { + console.log('Logging input events in search giphy'); + for (var event of events) { + console.log(event); + } + + const input = events[0]; + try { + const urlWithApiKey = 'http://api.giphy.com/v1/gifs/random?api_key=' + input.input_data.global_values.giphy_api_key; + const url = urlWithApiKey + '&tag=' + encodeURIComponent(input.payload.parameters); + const resp = await fetch(url, { method: 'GET' }); + + if (resp.ok) { + console.log('Fetched gif successfully'); + const respData: any = await resp.json(); + await CreateGiphySnapKit(input, respData.data.images); + } else { + let body = await resp.text(); + console.log('Error while fetching gif: ', resp.status, body); + } + } catch (error) { + console.log('Failed to fetch gif: ', error); + } +}; + +export default run; +``` + +## Run + +You can test the functions locally using the provided fixtures. + +### Testing the Slash Command + +1. Navigate to the `3-giphy-template/code` directory. +2. Install dependencies: + ```bash + npm install + ``` +3. Run the local test runner for the `search_giphy` function: + ```bash + npm run start:watch -- --functionName=search_giphy --fixturePath=command_event.json + ``` + +### Testing the Automation + +To test the automation that posts a GIF when an issue is closed, you can run: +```bash +npm run start:watch -- --functionName=publish_giphy_on_work_closed --fixturePath=publish_giphy_on_work_closed.json +``` + +## Verify + +When you run the `search_giphy` function locally, you will see the following initial output in your console: + +``` +Logging input events in search giphy +{ + "payload": { + "actor_id": "don:identity:dvrv-us-1:devo/0:devu/1", + "command_id": "don:integration:dvrv-us-1:devo/0:namespace/cns:command/cname", + "dev_org": "don:identity:dvrv-us-1:devo/0", + "parameters": "commands parameters string passed by user", + "parent_id": "don:integration:dvrv-us-1:devo/0:snap_in/00000001-0001-0001-0001-00000001", + "request_id": "4QtCBSKJcKKqwQhoJKZvRQ", + "source_id": "don:core:dvrv-us-1:devo/0:issue/1" + }, + "context": { + "dev_oid": "don:identity:dvrv-us-1:devo/0", + "source_id": "don:integration:dvrv-us-1:devo/0:namespace/kapil:command/complete_comm_k", + "snap_in_id": "don:integration:dvrv-us-1:devo/0:snap_in/00000001-0001-0001-0001-00000001", + "snap_in_version_id": "don:integration:dvrv-us-1:devo/0:snap_in_package/00000001-0001-0001-0001-00000001:snap_in_version/00000001-0001-0001-0001-00000001" + }, + "execution_metadata": { + "request_id": "4QtCBSKJcKKqwQhoJKZvRQ", + "function_name": "foobar" + }, + "input_data": { + "global_values": {}, + "event_sources": {}, + "keyrings": null + } +} +``` + +> **Note:** The function will then attempt to call the Giphy API. This will fail in the local run because the `giphy_api_key` is not provided in the fixture. To fully test the functionality, you must install and configure the snap-in in your DevRev organization with a valid API key. diff --git a/codelabs/4-sample-snap-in.mdx b/codelabs/4-sample-snap-in.mdx new file mode 100644 index 0000000..740c6e9 --- /dev/null +++ b/codelabs/4-sample-snap-in.mdx @@ -0,0 +1,223 @@ +--- +title: 'Sample Snap-in' +description: 'A sample snap-in demonstrating automations for new work items and a custom slash command to post comments.' +--- + +## Setup + +This section guides you on setting up the snap-in. + +### Prerequisites + +- Node.js and `npm` installed. +- A DevRev account with the CLI installed and configured. + +### 1. Get the Code + +You can start by using the code in the `4-sample-snap-in` directory. + +### 2. Configure the Snap-in + +The automation in this snap-in can be customized using input fields. These are defined in the `manifest.yaml` and can be set during installation. The default values will post a comment with the text "Hello World is printed on the work [work-id] from the automation, with message: Message from the input field. name1 name2". + +## Manifest + +The `manifest.yaml` file defines the automation, the slash command, and the input fields for customizing the automation's comment. + +```yaml +version: '2' + +name: Sample Snap-in for DevRev Hackathon +description: Snap In to add Comments for demonstration purpose. + +service_account: + display_name: "DevRev Bot" + +event_sources: + organization: + - name: devrev-webhook + display_name: DevRev + type: devrev-webhook + config: + event_types: + - work_created + +inputs: + organization: + - name: input_field_1 + description: Input field to add comment to the work item. + field_type: text + default_value: "Message from the input field." + ui: + display_name: Input Field 1 + + - name: input_field_2 + description: Add extra comment. + field_type: bool + default_value: true + ui: + display_name: Should extra comment be added? + + - name: input_field_array + description: List of names to add as comment. + base_type: text + field_type: array + default_value: ["name1", "name2"] + ui: + display_name: List of extra names + +functions: + - name: function_1 + description: Function to create a timeline entry comment on a DevRev work item created. + - name: function_2 + description: Function to create a timeline entry comment on a DevRev work item on which comment is added. + +automations: + - name: convergence_automation_devrev + source: devrev-webhook + event_types: + - work_created + function: function_1 + +commands: + - name: comment_here + namespace: devrev + description: Command to trigger function to add comment to this work item. + surfaces: + - surface: discussions + object_types: + - issue + - ticket + usage_hint: "Command to add comment to this work item." + function: function_2 +``` + +## Code + +This snap-in contains two functions: one for the automation and one for the slash command. + +### Automation: `function_1` + +This function is triggered by a `work_created` event and posts a comment constructed from the input fields. The code is located in `4-sample-snap-in/code/src/functions/function_1/index.ts`. + +```typescript +import { client } from "@devrev/typescript-sdk"; + +async function handleEvent( + event: any, +) { + const devrevPAT = event.context.secrets.service_account_token; + const API_BASE = event.execution_metadata.devrev_endpoint; + const devrevSDK = client.setup({ + endpoint: API_BASE, + token: devrevPAT, + }) + const workCreated = event.payload.work_created.work; + const messageInput = event.input_data.global_values.input_field_1; + let bodyComment = 'Hello World is printed on the work ' + workCreated.display_id + ' from the automation, with message: ' + messageInput; + const extraComment = event.input_data.global_values.input_field_2; + const extraNames = event.input_data.global_values.input_field_array; + if (extraComment) { + for (let name of extraNames) { + bodyComment = bodyComment + ' ' + name; + } + } + const body = { + object: workCreated.id, + type: 'timeline_comment', + body: bodyComment, + } + const response = await devrevSDK.timelineEntriesCreate(body as any); + return response; + +} + +export const run = async (events: any[]) => { + console.info('events', JSON.stringify(events), '\n\n\n'); + for (let event of events) { + const resp = await handleEvent(event); + console.log(JSON.stringify(resp.data)); + } +}; + +export default run; +``` + +### Slash Command: `function_2` + +This function is triggered by the `/comment_here` slash command and posts a simple "Hello World" comment. The code is located in `4-sample-snap-in/code/src/functions/function_2/index.ts`. + +```typescript +import { client } from "@devrev/typescript-sdk"; + +async function handleEvent( + event: any, +) { + const devrevPAT = event.context.secrets.service_account_token; + const API_BASE = event.execution_metadata.devrev_endpoint; + const devrevSDK = client.setup({ + endpoint: API_BASE, + token: devrevPAT, + }) + const workCreated = event.payload.source_id; + const bodyComment = 'Hello World is printed on the work from the command.'; + const body = { + object: workCreated, + type: 'timeline_comment', + body: bodyComment, + } + const response = await devrevSDK.timelineEntriesCreate(body as any); + return response; + +} + +export const run = async (events: any[]) => { + console.info('events', JSON.stringify(events), '\n\n\n'); + for (let event of events) { + const resp = await handleEvent(event); + console.log(JSON.stringify(resp.data)); + } +}; + +export default run; +``` + +## Run + +You can test the functions locally using the provided fixtures. + +1. Navigate to the `4-sample-snap-in/code` directory. +2. Install dependencies: + ```bash + npm install + ``` +3. To test the automation (`function_1`), run: + ```bash + npm run start:watch -- --functionName=function_1 --fixturePath=function_1_event.json + ``` +4. To test the slash command (`function_2`), run: + ```bash + npm run start:watch -- --functionName=function_2 --fixturePath=function_2_event.json + ``` + +## Verify + +When you run the functions locally, you will see the event payload logged to the console. + +### `function_1` Verification + +The console will log the full event from the `function_1_event.json` fixture. Due to the length of the event, only a portion is shown here. + +```json +info: events "[{\\"payload\\":{\\"id\\":\\"don:integration:dvrv-us-1:devo/XXXXXX:webhook/ovtfa4mg:webhook_event/uzX6Pqe9tQc\\",\\"timestamp\\":\\"2023-08-03T06:03:21.932268Z\\",\\"type\\":\\"work_created\\", ...}]" +``` + +### `function_2` Verification + +The console will log the full event from the `function_2_event.json` fixture. + +```json +info: events "[{\\"payload\\":{\\"actor_id\\":\\"don:identity:dvrv-us-1:devo/XXXXXX:devu/1\\",\\"command_id\\":\\"don:integration:dvrv-us-1:devo/XXXXXX:namespace/devrev:command/comment_here\\", ...}]" +``` + +> **Note:** After logging the event, both functions will attempt to post a comment using the DevRev API. The response from this API call will also be logged. The exact response will vary depending on the execution environment. diff --git a/codelabs/5-custom-webhook.mdx b/codelabs/5-custom-webhook.mdx new file mode 100644 index 0000000..655522c --- /dev/null +++ b/codelabs/5-custom-webhook.mdx @@ -0,0 +1,118 @@ +--- +title: 'Custom Webhook Integration' +description: 'A snap-in demonstrating how to integrate DevRev with external systems by receiving and processing events through a custom webhook.' +--- + +## Setup + +This section guides you on setting up the custom webhook snap-in. + +### Prerequisites + +- Node.js and `npm` installed. +- A DevRev account with the CLI installed and configured. +- An external system capable of sending HTTP POST requests (webhooks). + +### 1. Get the Code + +You can start by using the code in the `5-custom-webhook` directory. + +### 2. Configure the Snap-in + +After installing this snap-in, you need to configure your external system to send webhooks to the URL provided during the installation. The `manifest.yaml` provides these instructions in the `setup_instructions` field. The payload should be a JSON object with `work_created` and `body` keys. + +## Manifest + +The `manifest.yaml` file defines the custom webhook event source and the automation that connects it to the `on_work_creation` function. + +```yaml +version: "1" + +name: "External Event Source" +description: "Sample external event source snap in" + +service_account: + display_name: "External Event Bot" + +event-sources: + - name: external-alerts + description: Event coming from external source + display_name: External Event + type: flow-custom-webhook + setup_instructions: | + ## External Event Webhook + + 1. Enter the webhook URL as `{{source.trigger_url}}` + 2. Enter the webhook PAYLOAD as + `{ + 'work_created': , + 'body': , + }` + config: + policy: | + package rego + output = {"event": event, "event_key": event_key} { + event := input.request.body + event_key := "external.alert-event" + } + +functions: + - name: on_work_creation + description: Function to send notification to potentially relevant users. + +automations: + - name: Send notification on External Event alerts + source: external-alerts + event_types: + - custom:external.alert-event + function: on_work_creation +``` + +## Code + +The `5-custom-webhook/code/src/functions/on_work_creation/index.ts` file contains the function triggered by the custom webhook. It extracts the `work_created` ID and `body` from the payload to create a new timeline comment. + +```typescript +async function handleEvent( + event: any, +) { + const devrevPAT = event.context.secrets.service_account_token; + const API_BASE = event.execution_metadata.devrev_endpoint; + const workCreated = event.payload.work_created; + const bodyComment = event.payload.body; + const body = { + object: workCreated, + type: 'timeline_comment', + body: bodyComment, + } + const response = await postCallAPI(API_BASE + '/timeline-entries.create', body, devrevPAT); + if (!response.success) { + console.log(response.errMessage); + return response; + } + console.log(response.data); + return response; +} +``` + +## Run + +You can test the function locally using the provided fixture. + +1. Navigate to the `5-custom-webhook/code` directory. +2. Install dependencies: + ```bash + npm install + ``` +3. Run the local test runner: + ```bash + npm run start:watch -- --functionName=on_work_creation --fixturePath=on_work_created_event.json + ``` + +## Verify + +When you run the function locally, it will attempt to post a timeline comment to the work item specified in the fixture (`don:core:dvrv-us-1:devo/XXXX:issue/25`). + +The function logs the response from the DevRev API. The exact output will vary, but a successful run will log the data of the newly created timeline entry. If the API call fails (for example, due to an invalid token or work ID), an error message will be logged instead. + +To fully verify, you can check the timeline of the specified work item in the DevRev UI for the new comment. diff --git a/codelabs/6-timer-ticket-creator.mdx b/codelabs/6-timer-ticket-creator.mdx new file mode 100644 index 0000000..c643ec5 --- /dev/null +++ b/codelabs/6-timer-ticket-creator.mdx @@ -0,0 +1,116 @@ +--- +title: 'Timer-based Ticket Creator' +description: 'A snap-in that demonstrates how to create timer-based automations that perform actions on a schedule, such as creating a ticket every 10 minutes.' +--- + +## Setup + +This section guides you on setting up the timer-based ticket creator snap-in. + +### Prerequisites + +- Node.js and `npm` installed. +- A DevRev account with the CLI installed and configured. + +### 1. Get the Code + +You can start by using the code in the `6-timer-ticket-creator` directory. + +### 2. Configure the Snap-in + +This snap-in is configured to create a ticket every 10 minutes. You can change the schedule by modifying the `cron` expression in the `manifest.yaml` file. The code also hardcodes the `applies_to_part` and `owned_by` values. You will likely need to change `'PROD-1'` and `'DEVU-1'` to match a valid part and user in your organization. + +## Manifest + +The `manifest.yaml` file defines the timer event source and the automation that creates the tickets. The `cron` expression `*/10 * * * *` tells the system to trigger the automation every 10 minutes. + +```yaml +version: "2" + +name: "Timely Ticketer" +description: "Snap-in to create ticket every 10 minutes" + +service_account: + display_name: Automatic Ticket Creator Bot + +event_sources: + organization: + - name: timer-event-source + description: Event source that sends events every 10 minutes. + display_name: Timer Event Source + type: timer-events + config: + # CRON expression for triggering every 10 minutes. + cron: "*/10 * * * *" + metadata: + event_key: ten_minute_event + +functions: + - name: ticket_creator + description: Function to create a new ticket when triggered. + +automations: + - name: periodic_ticket_creator + description: Automation to create a ticket every 10 minutes + source: timer-event-source + event_types: + - timer.tick + function: ticket_creator +``` + +## Code + +The `6-timer-ticket-creator/code/src/functions/ticket_creator/index.ts` file contains the function that is executed by the timer automation. It uses the DevRev SDK to create a new ticket with a timestamped title and body. + +```typescript +import { client, publicSDK } from '@devrev/typescript-sdk'; + +export const run = async (events: any[]) => { + for (const event of events) { + const endpoint = event.execution_metadata.devrev_endpoint; + const token = event.context.secrets.service_account_token; + + // Initialize the public SDK client + const devrevSDK = client.setup({ endpoint, token }); + + // Create a ticket. Name the ticket using the current date and time. + const date = new Date(); + const ticketName = `Ticket created at ${date.toLocaleString()}`; + const ticketBody = `This ticket was created by a snap-in at ${date.toLocaleString()}`; + + const reponse = await devrevSDK.worksCreate({ + title: ticketName, + body: ticketBody, + // The ticket will be created in the PROD-1 part. Rename this to match your part. + applies_to_part: 'PROD-1', + // The ticket will be owned by the DEVU-1 team. Rename this to match the required user. + owned_by: ['DEVU-1'], + type: publicSDK.WorkType.Ticket, + }); + + console.log(reponse); + } +}; +``` + +## Run + +While this snap-in is designed to run on a timer, you can test the `ticket_creator` function locally using the provided fixture, which simulates a `timer.tick` event. + +1. Navigate to the `6-timer-ticket-creator/code` directory. +2. Install dependencies: + ```bash + npm install + ``` +3. Run the local test runner: + ```bash + npm run start:watch -- --functionName=ticket_creator --fixturePath=timer-tick.json + ``` + +## Verify + +When you run the function locally, it will attempt to create a new ticket using the DevRev API. The function logs the entire response from the API call. + +A successful run will log an object containing the details of the newly created ticket. If the API call fails (for example, due to an invalid token, part, or owner ID), an error object will be logged instead. + +In a live environment, you can verify that the snap-in is working by checking for new tickets in the specified part every 10 minutes. diff --git a/codelabs/7-googleplaystore-reviews-ingestion.mdx b/codelabs/7-googleplaystore-reviews-ingestion.mdx new file mode 100644 index 0000000..eac5240 --- /dev/null +++ b/codelabs/7-googleplaystore-reviews-ingestion.mdx @@ -0,0 +1,309 @@ +--- +title: 'Google Play Store Review Ingestion' +description: 'An automation that fetches Google Play Store reviews, uses an LLM to categorize them, and creates tickets in DevRev.' +--- + +## Setup + +This section guides you on setting up the Google Play Store review ingestion snap-in. + +### Prerequisites + +- Node.js and `npm` installed. +- A DevRev account with the CLI installed and configured. +- A Fireworks AI API key from the [Fireworks AI website](https://readme.fireworks.ai/docs/quickstart). + +### 1. Get the Code + +You can start by using the code in the `7-googleplaystore-reviews-ingestion` directory. + +### 2. Configure the Snap-in + +This snap-in requires several inputs to be configured upon installation: +- **Application ID:** The Google Play ID of your application. +- **Default Part:** The DevRev part where new tickets will be created. +- **Default Owner:** The user or team who will own the new tickets. +- **Fireworks AI API Key:** Your API key for the LLM service. +- **LLM Model:** The specific large language model to use for categorization. + +## Manifest + +The `manifest.yaml` file defines the slash command, the required inputs (including the Fireworks AI keyring), and the tags used for categorization. + +```yaml +version: "2" +name: "Google playstore reviews to Tickets" +description: "Creates tickets from Google playstore reviews and categorize them into one-of `bug`, `feedback`, `feature_request` or `question`." + +service_account: + display_name: Google Playstore Reviews Snap-in + +keyrings: + organization: + - name: fireworks_api_key + description: API Key for Fireworks, follow https://readme.fireworks.ai/docs/quickstart to get one. + types: + - snap_in_secret + display_name: Fireworks API Key + +inputs: + organization: + - name: app_id + description: "The Google Play id of the application (the ?id= parameter on the url)." + field_type: text + is_required: true + default_value: "" + ui: + display_name: Application ID + - name: default_part_id + description: "Default part under which to create tickets." + field_type: id + id_type: + - product + - capability + - feature + - enhancement + is_required: true + default_value: "don:core:dvrv-us-1:devo/xxx:product/xxx" + ui: + display_name: Default Part + - name: default_owner_id + description: "Default owner of the tickets." + field_type: id + id_type: + - devu + is_required: true + default_value: "don:identity:dvrv-us-1:devo/xxx:devu/xxx" + ui: + display_name: Default Owner + - name: llm_model_to_use + description: "Which LLM model to use for the review categorization. Not all might work perfectly, generally prefer a larger model with >= 7B params" + field_type: enum + allowed_values: + - qwen-72b-chat + - elyza-japanese-llama-2-7b-fast-instruct + - firellava-13b + - japanese-llava-mistral-7b + - japanese-stablelm-instruct-beta-70b + - japanese-stablelm-instruct-gamma-7b + - japanese-stable-vlm + - llamaguard-7b + - llama-v2-13b + - llama-v2-13b-chat + - llama-v2-13b-code + - llama-v2-13b-code-instruct + - llama-v2-34b-code + - llama-v2-34b-code-instruct + - llama-v2-70b + - llama-v2-70b-chat + - llama-v2-7b + - llama-v2-7b-chat + - llava-codellama-34b + - llava-v15-13b-fireworks + - mistral-7b + - mistral-7b-instruct-4k + - mixtral-8x7b + - mixtral-8x7b-instruct + - qwen-14b-chat + - qwen-1-8b-chat + - stablecode + - stablelm-zephyr-3b + - starcoder-16b-w8a16 + - starcoder-7b-w8a16 + - yi-34b-200k-capybara + - yi-6b + - zephyr-7b-beta + is_required: true + default_value: "mixtral-8x7b-instruct" + ui: + display_name: LLM Model to use. + + +tags: + - name: bug + description: "This is a bug" + - name: feature_request + description: "This is a feature request" + - name: question + description: "This is a question" + - name: feedback + description: "This is a feedback" + - name: failed_to_infer_category + description: "Failed to infer category" + + +commands: + - name: playstore_reviews_process + namespace: devrev + description: Fetches reviews from Google Playstore and creates tickets + surfaces: + - surface: discussions + object_types: + - snap_in + usage_hint: "/playstore_reviews_process [number of reviews to fetch and process]" + function: process_playstore_reviews + + +functions: + - name: process_playstore_reviews + description: Fetches reviews from Google Playstore and creates tickets +``` + +## Code + +The `7-googleplaystore-reviews-ingestion/code/src/functions/process_playstore_reviews/index.ts` file contains the logic for fetching and processing the reviews. It uses the `google-play-scraper` library to fetch reviews and calls the Fireworks AI LLM to categorize them before creating tickets in DevRev. + +```typescript +import {publicSDK } from '@devrev/typescript-sdk'; +import * as gplay from "google-play-scraper"; +import { ApiUtils, HTTPResponse } from './utils'; +import {LLMUtils} from './llm_utils'; + +export const run = async (events: any[]) => { + for (const event of events) { + const endpoint: string = event.execution_metadata.devrev_endpoint; + const token: string = event.context.secrets.service_account_token; + const fireWorksApiKey: string = event.input_data.keyrings.fireworks_api_key; + const apiUtil: ApiUtils = new ApiUtils(endpoint, token); + // Get the number of reviews to fetch from command args. + const snapInId = event.context.snap_in_id; + const devrevPAT = event.context.secrets.service_account_token; + const baseURL = event.execution_metadata.devrev_endpoint; + const inputs = event.input_data.global_values; + let parameters:string = event.payload.parameters.trim(); + const tags = event.input_data.resources.tags; + const llmUtil: LLMUtils = new LLMUtils(fireWorksApiKey, `accounts/fireworks/models/${inputs['llm_model_to_use']}`, 200); + let numReviews = 10; + let commentID : string | undefined; + if (parameters === 'help') { + // Send a help message in CLI help format. + const helpMessage = `playstore_reviews_process - Fetch reviews from Google Play Store and create tickets in DevRev.\n\nUsage: /playstore_reviews_process \n\n\`number_of_reviews_to_fetch\`: Number of reviews to fetch from Google Playstore. Should be a number between 1 and 100. If not specified, it defaults to 10.`; + let postResp = await apiUtil.postTextMessageWithVisibilityTimeout(snapInId, helpMessage, 1); + if (!postResp.success) { + console.error(`Error while creating timeline entry: ${postResp.message}`); + continue; + } + continue + } + let postResp: HTTPResponse = await apiUtil.postTextMessageWithVisibilityTimeout(snapInId, 'Fetching reviews from Playstore', 1); + if (!postResp.success) { + console.error(`Error while creating timeline entry: ${postResp.message}`); + continue; + } + if (!parameters) { + // Default to 10 reviews. + parameters = '10'; + } + try { + numReviews = parseInt(parameters); + + if (!Number.isInteger(numReviews)) { + throw new Error('Not a valid number'); + } + } catch (err) { + postResp = await apiUtil.postTextMessage(snapInId, 'Please enter a valid number', commentID); + if (!postResp.success) { + console.error(`Error while creating timeline entry: ${postResp.message}`); + continue; + } + commentID = postResp.data.timeline_entry.id; + } + // Make sure number of reviews is <= 100. + if (numReviews > 100) { + postResp = await apiUtil.postTextMessage(snapInId, 'Please enter a number less than 100', commentID); + if (!postResp.success) { + console.error(`Error while creating timeline entry: ${postResp.message}`); + continue; + } + commentID = postResp.data.timeline_entry.id; + } + // Call google playstore scraper to fetch those number of reviews. + let getReviewsResponse:any = await gplay.reviews({ + appId: inputs['app_id'], + sort: gplay.sort.RATING, + num: numReviews, + throttle: 10, + }); + // Post an update about the number of reviews fetched. + postResp = await apiUtil.postTextMessageWithVisibilityTimeout(snapInId, `Fetched ${numReviews} reviews, creating tickets now.`, 1); + if (!postResp.success) { + console.error(`Error while creating timeline entry: ${postResp.message}`); + continue; + } + commentID = postResp.data.timeline_entry.id; + let reviews:gplay.IReviewsItem[] = getReviewsResponse.data; + // For each review, create a ticket in DevRev. + for(const review of reviews) { + // Post a progress message saying creating ticket for review with review URL posted. + postResp = await apiUtil.postTextMessageWithVisibilityTimeout(snapInId, `Creating ticket for review: ${review.url}`, 1); + if (!postResp.success) { + console.error(`Error while creating timeline entry: ${postResp.message}`); + continue; + } + const reviewText = `Ticket created from Playstore review ${review.url}\n\n${review.text}`; + const reviewTitle = review.title || `Ticket created from Playstore review ${review.url}`; + const reviewID = review.id; + const systemPrompt = `You are an expert at labelling a given Google Play Store Review as bug, feature_request, question or feedback. You are given a review provided by a user for the app ${inputs['app_id']}. You have to label the review as bug, feature_request, question or feedback. The output should be a JSON with fields "category" and "reason". The "category" field should be one of "bug", "feature_request", "question" or "feedback". The "reason" field should be a string explaining the reason for the category. \n\nReview: {review}\n\nOutput:`; + const humanPrompt = ``; + + let llmResponse = {}; + try { + llmResponse = await llmUtil.chatCompletion(systemPrompt, humanPrompt, {review: (reviewTitle ? reviewTitle + '\n' + reviewText: reviewText)}) + } catch (err) { + console.error(`Error while calling LLM: ${err}`); + } + let tagsToApply = []; + let inferredCategory = 'failed_to_infer_category'; + if ('category' in llmResponse) { + inferredCategory = llmResponse['category'] as string; + if (!(inferredCategory in tags)) { + inferredCategory = 'failed_to_infer_category'; + } + } + // Create a ticket with title as review title and description as review text. + const createTicketResp = await apiUtil.createTicket({ + title: reviewTitle, + tags: [{id: tags[inferredCategory].id}], + body: reviewText, + type: publicSDK.WorkType.Ticket, + owned_by: [inputs['default_owner_id']], + applies_to_part: inputs['default_part_id'], + }); + if (!createTicketResp.success) { + console.error(`Error while creating ticket: ${createTicketResp.message}`); + continue; + } + // Post a message with ticket ID. + const ticketID = createTicketResp.data.work.id; + const ticketCreatedMessage = inferredCategory != 'failed_to_infer_category' ? `Created ticket: <${ticketID}> and it is categorized as ${inferredCategory}` : `Created ticket: <${ticketID}> and it failed to be categorized`; + const postTicketResp: HTTPResponse = await apiUtil.postTextMessageWithVisibilityTimeout(snapInId, ticketCreatedMessage, 1); + if (!postTicketResp.success) { + console.error(`Error while creating timeline entry: ${postTicketResp.message}`); + continue; + } + } + // Call an LLM to categorize the review as Bug, Feature request, or Question. + } +}; + +export default run; +``` + +## Run + +This snap-in is designed to be run from within the DevRev UI after it has been installed and configured. + +1. Go to a discussion in DevRev. +2. Type the slash command `/playstore_reviews_process [number]` and press Enter. For example, to fetch 20 reviews, you would type: + ``` + /playstore_reviews_process 20 + ``` + +> **Note:** Due to the reliance on external services (Google Play Store, Fireworks AI) and the need for specific configuration (App ID, API Keys), this snap-in cannot be effectively tested with a local fixture. + +## Verify + +After running the slash command, the snap-in will post a series of status updates in the discussion. Once it has finished processing, you can verify its execution by: + +1. **Checking for new tickets:** Navigate to the part you configured as the "Default Part". You should see new tickets created for each review fetched from the Google Play Store. +2. **Checking the tags:** Each new ticket should be tagged as `bug`, `feature_request`, `question`, or `feedback` based on the LLM's categorization. If the categorization fails, it will be tagged as `failed_to_infer_category`. diff --git a/codelabs/8-external-github-webhook.mdx b/codelabs/8-external-github-webhook.mdx new file mode 100644 index 0000000..fda1c68 --- /dev/null +++ b/codelabs/8-external-github-webhook.mdx @@ -0,0 +1,169 @@ +--- +title: 'GitHub Webhook Integration' +description: 'A snap-in that integrates DevRev with GitHub using webhooks, posting commit messages from a repository to a discussion in DevRev.' +--- + +## Setup + +This section guides you on setting up the GitHub webhook snap-in. + +### Prerequisites + +- Node.js and `npm` installed. +- A DevRev account with the CLI installed and configured. +- A GitHub repository where you can configure webhooks. + +### 1. Get the Code + +You can start by using the code in the `8-external-github-webhook` directory. + +### 2. Configure the Snap-in + +This snap-in requires you to set up a webhook in your GitHub repository. +1. After installing the snap-in, DevRev will provide a **Webhook URL** and a **Secret**. +2. In your GitHub repository settings, go to **Webhooks** and add a new webhook. +3. Paste the URL into the "Payload URL" field. +4. Paste the secret into the "Secret" field. +5. Set the "Content type" to `application/json`. +6. Choose to send `push` events. +7. Activate the webhook. + +You also need to configure the `part_id` input field in the manifest to specify which part's discussion will receive the commit messages. + +## Manifest + +The `manifest.yaml` file defines the custom webhook event source, including a Rego policy for validating the webhook signature to ensure the request is from GitHub. + +```yaml +version: "2" + +name: GitHub Commit Tracker +description: Reflects commits that happen on GitHub in DevRev by posting to timeline of a product part. + +service_account: + display_name: "GitHub-Commit Bot" + +inputs: + organization: + - name: part_id + field_type: id + default_value: don:core:dvrv-us-1:devo/XXXXXXX:product/1 + is_required: true + id_type: + - product + description: The default part on which to post commits. + ui: + display_name: The part on which to post commits. + +event_sources: + organization: + - name: github-app-source + type: flow-custom-webhook + description: Event coming from Github app. + config: + policy: | + package rego + signature := crypto.hmac.sha256(base64.decode(input.request.body_raw), input.parameters.secret) + expected_header := sprintf("sha256=%v", [signature]) + signature_header_name:= "X-Hub-Signature-256" + status_code = 200 { + input.request.headers[signature_header_name] == expected_header + } else = 401 { + true + } + output = {"event": body, "event_key": event_key} { + status_code == 200 + body := input.request.body + event_key := "github-event" + } else = {"response": response} { + response := {"status_code": status_code} + } + parameters: + secret: 6aVqEymevGZvkiUk30oWccVLEKNOqkcP + setup_instructions: "Please copy the source URL from here: \n\nURL: `{{ source.trigger_url }}` \n\nSecret: `{{source.config.parameters.secret}}`." + +functions: + - name: github_handler + description: Function to reflect Github activities on DevRev. + +automations: + - name: github-commit-tracker + source: github-app-source + event_types: + - custom:github-event + function: github_handler +``` + +## Code + +The `8-external-github-webhook/code/src/functions/github_handler/index.ts` file contains the function triggered by the GitHub webhook. It extracts commit messages from the payload and posts them to the specified part. + +```typescript +import { client, betaSDK } from '@devrev/typescript-sdk'; + +// Handles the event from GitHub +async function handleEvent(event: any) { + // Extract necessary information from the event + const token = event.context.secrets['service_account_token']; + const endpoint = event.execution_metadata.devrev_endpoint; + + // Set up the DevRev SDK with the extracted information + const devrevSDK = client.setup({ + endpoint: endpoint, + token: token, + }); + + // Extract the part ID and commits from the event + const partID = event.input_data.global_values['part_id']; + const commits = event.payload['commits']; + + // Iterate through commits and append the commit message to the body of the comment + let bodyComment = 'Commits from GitHub:\n'; + for (const commit of commits) { + bodyComment += commit.message + '\n'; + } + + // Prepare the body for creating a timeline comment + const body: betaSDK.TimelineEntriesCreateRequest = { + body: bodyComment, + object: partID, + type: betaSDK.TimelineEntriesCreateRequestType.TimelineComment, + }; + + // Create a timeline comment using the DevRev SDK + const response = await devrevSDK.timelineEntriesCreate(body); + + // Return the response from the DevRev API + return response; +} + +export const run = async (events: any[]) => { + for (const event of events) { + await handleEvent(event); + } +}; + +export default run; +``` + +## Run + +While this snap-in is designed to be triggered by a real GitHub webhook, you can test the `github_handler` function locally using the provided fixture, which simulates a `push` event. + +1. Navigate to the `8-external-github-webhook/code` directory. +2. Install dependencies: + ```bash + npm install + ``` +3. Run the local test runner: + ```bash + npm run start:watch -- --functionName=github_handler --fixturePath=github_event.json + ``` + +## Verify + +The `github_handler` function does not log any output to the console. It directly calls the DevRev API to create a timeline comment. + +To verify that the local run is working, you would need to provide a valid `service_account_token` in the fixture and a valid `part_id` as a global value. After running the command, you would then check the discussion of the specified part in the DevRev UI for a new comment containing the commit messages from the `github_event.json` fixture. + +For a live environment, push a commit to your configured GitHub repository and check the part's discussion in DevRev. diff --git a/codelabs/9-external-action.mdx b/codelabs/9-external-action.mdx new file mode 100644 index 0000000..186ab45 --- /dev/null +++ b/codelabs/9-external-action.mdx @@ -0,0 +1,227 @@ +--- +title: 'Create GitHub Issues from DevRev' +description: 'A snap-in that provides a slash command to create a GitHub issue directly from a DevRev issue.' +--- + +## Setup + +This section guides you on setting up the GitHub issue creator snap-in. + +### Prerequisites + +- Node.js and `npm` installed. +- A DevRev account with the CLI installed and configured. +- A GitHub Personal Access Token (PAT) with `repo` scope. + +### 1. Get the Code + +You can start by using the code in the `9-external-action` directory. + +### 2. Configure the Snap-in + +This snap-in requires a GitHub Personal Access Token (PAT) to authorize issue creation. During installation, you will be prompted to provide this token, which will be stored securely in the `github_connection` keyring defined in the manifest. + +## Manifest + +The `manifest.yaml` file defines the `/gh_issue` slash command and the `github_connection` keyring for securely storing the GitHub PAT. + +```yaml +version: "2" +name: "GitHub Issue Creator" +description: "Create a GitHub issue from work in DevRev." + +service_account: + display_name: GitHub Issue Creator + +keyrings: + organization: + - name: github_connection + display_name: Github Connection + description: Github PAT + types: + - snap_in_secret + +functions: + - name: command_handler + description: function to create a GitHub issue + +commands: + - name: gh_issue + namespace: devrev + description: Command to create a GitHub issue. + surfaces: + - surface: discussions + object_types: + - issue + usage_hint: "[OrgName] [RepoName]" + function: command_handler +``` + +## Code + +The logic for the snap-in is located in `9-external-action/code/src/functions/command_handler/index.ts`. The `run` function is triggered by the `/gh_issue` command. It uses the DevRev SDK to get the details of the source issue and the Octokit library to create a new issue in the specified GitHub repository. + +```typescript +import { client, publicSDK } from '@devrev/typescript-sdk'; +import { Octokit } from '@octokit/core'; + +type IssueDetails = { + description: string | undefined; + issueDisplayName: string | undefined; + title: string; +}; + +// Function to get the title and description of the issue +const getIssueDetails = async (workId: string, devrevSDK: publicSDK.Api) => { + try { + // Get the issue details using the `worksGet` method + const workItemResp = await devrevSDK.worksGet({ + id: workId, + }); + const workItem = workItemResp.data.work; + + // Populate the issue details + const issueDetails: IssueDetails = { + description: workItem.body, + issueDisplayName: workItem.display_id, + title: workItem.title, + }; + return issueDetails; + } catch (error) { + console.error(error); + throw new Error('Failed to get issue details'); + } +}; + +// Function to retrieve Organisation name and repository name from command parameters +const getOrgAndRepoNames = (paramString: string): string[] => { + const paramList = paramString.split(' '); + if (paramList.length !== 2) { + throw new Error('Invalid Parameters'); + } + const [orgName, repoName] = paramList; + return [orgName, repoName]; +}; + +// Function to verify if the orgName is valid +const verifyOrgName = async (orgName: string, octokit: Octokit): Promise => { + try { + await octokit.request('GET /orgs/{org}', { + headers: { + 'X-GitHub-Api-Version': '2022-11-28', + }, + org: orgName, + }); + } catch (error) { + console.error(error); + throw new Error('Invalid Organisation Name'); + } +}; + +// Function to verify if the repoName is valid +const verifyRepoName = async (orgName: string, repoName: string, octokit: Octokit): Promise => { + try { + await octokit.request('GET /repos/{owner}/{repo}', { + headers: { + 'X-GitHub-Api-Version': '2022-11-28', + }, + owner: orgName, + repo: repoName, + }); + } catch (error) { + console.error(error); + throw new Error('Invalid Repository Name'); + } +}; + +// Function to create an issue +const createGitHubIssue = async ( + orgName: string, + repoName: string, + issueDetails: IssueDetails, + octokit: Octokit +): Promise => { + try { + await octokit.request('POST /repos/{owner}/{repo}/issues', { + body: issueDetails.description, + headers: { + 'X-GitHub-Api-Version': '2022-11-28', + }, + owner: orgName, + repo: repoName, + title: `[${issueDetails.issueDisplayName}] ${issueDetails.title}`, + }); + } catch (error) { + console.error(error); + throw new Error('Failed to create issue'); + } +}; + +// Function to handle the command event +const handleEvent = async (event: any) => { + // Get the github token from the environment variables and initialise the Octokit client. + const githubPAT = event.input_data.keyrings['github_connection']; + const octokit = new Octokit({ + auth: githubPAT, + }); + + // Get the devrev token and initialise the DevRev SDK. + const devrevToken = event.context.secrets['service_account_token']; + const endpoint = event.execution_metadata.devrev_endpoint; + const devrevSDK = client.setup({ + endpoint: endpoint, + token: devrevToken, + }); + + // Retrieve the Issue Details from the command event. + const workId = event.payload['source_id']; + const issueDetails = await getIssueDetails(workId, devrevSDK); + + // Get the command parameters from the event + const commandParams = event.payload['parameters']; + const [orgName, repoName] = getOrgAndRepoNames(commandParams); + + // Verify if orgName is valid + await verifyOrgName(orgName, octokit); + + // Verify if repoName is valid + await verifyRepoName(orgName, repoName, octokit); + + // Create an issue using the issue details + await createGitHubIssue(orgName, repoName, issueDetails, octokit); +}; + +export const run = async (events: any[]) => { + for (const event of events) { + await handleEvent(event); + } +}; + +export default run; +``` + +## Run + +While this snap-in is designed to be run from the DevRev UI, you can test the `command_handler` function locally using the provided fixture. + +1. Navigate to the `9-external-action/code` directory. +2. Install dependencies: + ```bash + npm install + ``` +3. Run the local test runner: + ```bash + npm run start:watch -- --functionName=command_handler --fixturePath=on_command.json + ``` + +## Verify + +The `command_handler` function does not log any output to the console upon successful execution. It will only log errors if an API call fails. + +To verify that the local run is working, you would need to provide valid secrets in the `on_command.json` fixture: +- A valid DevRev `service_account_token`. +- A valid GitHub PAT in the `github_connection` keyring. + +After running the command with valid secrets, you can verify its success by checking the specified GitHub repository for a newly created issue. + +For a live environment, run the `/gh_issue ` command on a DevRev issue and check the target repository.