Cycle's help center
LegalCompany
  • đź§±Core Documentation
    • 🚀Capturing Feedback
    • ⚡Processing Feedback
    • 📊Analyzing Data
    • 🤩Closing the Loop
  • 📚Guides
    • đź”—Integrations
      • HubSpot
      • Salesforce
      • Linear
      • GitHub
      • Notion
      • Slack
      • Intercom
      • Zapier
      • Call recording
      • Email
      • Chrome extension
      • Gong
      • Zendesk
      • Modjo
    • đź‘·Customer Sync
      • Customer Data Ingestion
      • Exporting Salesforce Customer data
      • Exporting HubSpot Customer data
    • 🌆Views
    • 📝AI-powered editor
  • 🥳Latest Features
    • đź§ Custom Prompts
    • đź“‚Product Areas
    • 📊Cycle Dashboards
    • 🤖Cycle Ask – Your On‑Demand Product Analyst
  • 🤔Guides
    • ⬇️Migrate your data to Cycle
      • Canny migration
    • Synchronise your customer data
  • đź“–Cycle Glossary
Powered by GitBook
On this page
  • What this script does
  • How to use the script
  • Code overview

Was this helpful?

  1. Guides
  2. Migrate your data to Cycle

Canny migration

PreviousMigrate your data to CycleNextSynchronise your customer data

Last updated 10 months ago

Was this helpful?

Importing data from Canny to Cycle has never been easier! This guide will walk you through using the Cycle API to seamlessly transfer your valuable customer feedback from Canny into your Cycle inbox alongside your features from your Canny roadmap.

For more details and to access the script, check out the . 🚀

What this script does

  • Fetches each Canny post from all your boards.

  • Retrieves votes and comments for each post.

  • Creates a document with the desired type (from config) for each post in Cycle.

  • Links feedback to each vote for the created document.

  • Tags all imported documents with an imported property, allowing you to filter or bulk delete these documents if needed.

How to use the script

Configure Your Data:

Fill your data in ./config.ts.

Run the Import Script:

Execute the command npm run import:canny.

Code overview

Let’s try to understand how the script works and why it’s done that way:

Cycle core logic

From your Cycle workspace, you get a unique slug, the script will proceed as follows

  1. Retrieve the “productId” from the slug (”product” is the old name for “workspace”)

  2. Create the import attribute (”attribute” is the tech name for “properties”) in order to let you filter on imported docs in views

  3. Link the attributes to doc type in order to assign the attribute value to imported docs

  4. Fetch Canny data and create docs out of it

Fetching Canny

The script defines a fetchCanny function to retrieve data from Canny's API. It handles pagination to ensure all posts, comments, and votes are fetched.

const fetchCanny = async <T>(
  endpoint: string,
  body = {},
  pagination = { skip: 0, limit: 10 }
): Promise<T> => {
  const requestOptions = {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      apiKey: cannyConfig.cannySecretKey,
      skip: pagination.skip,
      limit: pagination.limit,
      ...body,
    }),
  };

  const response = await fetch(`https://canny.io/api/v1/${endpoint}`, requestOptions);
  if (!response.ok) throw Error('Failed to fetch');
  const json = await response.json();
  return json;
};

Retrieving boards and posts

Let’s start by retrieving all your boards ans keep the one with posts only:

const retrieveBoards = async () => {
  const response = await fetchCanny<{ boards: Board[] }>('boards/list');
  return response.boards;
};

const filterBoardWithPosts = (boards: Board[]) => {
  return boards.filter((b) => b.postCount >= 1);
};

Fetching comments and votes

For each post, the script fetches related comments and votes:

const fetchAllComments = async (postID: string, skip: number = 0, accumulatedComments: Comment[] = []): Promise<Comment[]> => {
  const { hasMore, comments } = await fetchCanny<{ hasMore: boolean; comments: Comment[] }>('comments/list', { postID }, { limit: 100, skip });
  accumulatedComments.push(...comments);

  if (hasMore) {
    return fetchAllComments(postID, skip + comments.length, accumulatedComments);
  } else {
    return accumulatedComments;
  }
};

const fetchAllVotes = async (postID: string, skip: number = 0, accumulatedVotes: Vote[] = []): Promise<Vote[]> => {
  const { hasMore, votes } = await fetchCanny<{ hasMore: boolean; votes: Vote[] }>('votes/list', { postID }, { limit: 100, skip });
  accumulatedVotes.push(...votes);

  if (hasMore) {
    return fetchAllVotes(postID, skip + votes.length, accumulatedVotes);
  } else {
    return accumulatedVotes;
  }
};

Creating documents in Cycle

The script creates documents in Cycle for each post, along with linked feedback for each vote:

const createDocAndFeedback = async (workspaceId: string, post: Post, comments: Comment[], votes: Vote[], importAttributeData: any, docTypeToImport: any, insight: any) => {
  const createdDoc = await createDoc({
    workspaceId,
    attributes: [importAttributeData],
    contentJSON: getJSONDocContent({ post, comments: getFormattedComments(comments) }),
    doctypeId: docTypeToImport.id,
    title: post.title,
  });

  if (createdDoc) {
    for (const vote of votes) {
      const voteFeedback = await createFeedback({
        workspaceId,
        attributes: [importAttributeData],
        content: `Vote for ${post.title}`,
        sourceUrl: post.url,
        title: `Vote from ${vote.voter?.name || 'unknown'} on ${post.title}`,
        customerEmail: vote.voter.email,
      });
      await createDoc({
        doctypeId: insight.id,
        workspaceId,
        attributes: [importAttributeData],
        contentJSON: '',
        title: `Vote from ${vote.voter?.name || 'unknown'} on ${post.title}`,
        customerId: voteFeedback.customer.id,
        docSourceId: voteFeedback.id,
        parentId: createdDoc.id,
      });
    }
  }
};

As you can see, the way to have feedback connected to features in Cycle is using Insight concept which is a certain type of doc plus a link between feedback and feature. The link between a feedback and an insight is made via a doc containing a “docSourceId”. Then to link the insight to the feature, use a the “parentId” field.

So you always need to have the parent before the child in any relation import in Cycle.

The right order to proceed is then:

  1. Create the feature (Well the name is up to you since it’s fully customisable)

  2. Create the feedback

  3. Create the link between both (insight)

Hope this will help you to build crazy integrations with our API.

For more details and to access the script, check out the . 🚀

🤔
⬇️
code repository
code repository