mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-21 13:40:31 -06:00
Compare commits
2 Commits
feat/datab
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b70b2eef95 | ||
|
|
392a95834b |
@@ -61,6 +61,9 @@ describe("convertToEmbedUrl", () => {
|
||||
expect(convertToEmbedUrl("https://www.vimeo.com/123456789")).toBe(
|
||||
"https://player.vimeo.com/video/123456789"
|
||||
);
|
||||
expect(convertToEmbedUrl("https://player.vimeo.com/video/123456789")).toBe(
|
||||
"https://player.vimeo.com/video/123456789"
|
||||
);
|
||||
});
|
||||
|
||||
test("converts Loom URL to embed URL", () => {
|
||||
@@ -70,6 +73,9 @@ describe("convertToEmbedUrl", () => {
|
||||
expect(convertToEmbedUrl("https://loom.com/share/abcdef123456")).toBe(
|
||||
"https://www.loom.com/embed/abcdef123456"
|
||||
);
|
||||
expect(convertToEmbedUrl("https://www.loom.com/embed/abcdef123456")).toBe(
|
||||
"https://www.loom.com/embed/abcdef123456"
|
||||
);
|
||||
});
|
||||
|
||||
test("returns undefined for unsupported URLs", () => {
|
||||
@@ -109,6 +115,7 @@ describe("extractVimeoId", () => {
|
||||
test("extracts video ID from Vimeo URLs", () => {
|
||||
expect(extractVimeoId("https://vimeo.com/123456789")).toBe("123456789");
|
||||
expect(extractVimeoId("https://www.vimeo.com/123456789")).toBe("123456789");
|
||||
expect(extractVimeoId("https://player.vimeo.com/video/123456789")).toBe("123456789");
|
||||
});
|
||||
|
||||
test("returns null for invalid Vimeo URLs", () => {
|
||||
@@ -121,6 +128,7 @@ describe("extractLoomId", () => {
|
||||
test("extracts video ID from Loom URLs", () => {
|
||||
expect(extractLoomId("https://loom.com/share/abcdef123456")).toBe("abcdef123456");
|
||||
expect(extractLoomId("https://www.loom.com/share/abcdef123456")).toBe("abcdef123456");
|
||||
expect(extractLoomId("https://www.loom.com/embed/abcdef123456")).toBe("abcdef123456");
|
||||
});
|
||||
|
||||
test("returns null for invalid Loom URLs", async () => {
|
||||
|
||||
@@ -26,7 +26,7 @@ export const checkForVimeoUrl = (url: string): boolean => {
|
||||
|
||||
if (vimeoUrl.protocol !== "https:") return false;
|
||||
|
||||
const vimeoDomains = ["www.vimeo.com", "vimeo.com"];
|
||||
const vimeoDomains = ["www.vimeo.com", "vimeo.com", "player.vimeo.com"];
|
||||
const hostname = vimeoUrl.hostname;
|
||||
|
||||
return vimeoDomains.includes(hostname);
|
||||
@@ -74,7 +74,7 @@ export const extractYoutubeId = (url: string): string | null => {
|
||||
};
|
||||
|
||||
export const extractVimeoId = (url: string): string | null => {
|
||||
const regExp = /vimeo\.com\/(\d+)/;
|
||||
const regExp = /vimeo\.com\/(?:video\/)?(\d+)/;
|
||||
const match = regExp.exec(url);
|
||||
|
||||
if (match?.[1]) {
|
||||
@@ -85,7 +85,7 @@ export const extractVimeoId = (url: string): string | null => {
|
||||
};
|
||||
|
||||
export const extractLoomId = (url: string): string | null => {
|
||||
const regExp = /loom\.com\/share\/([a-zA-Z0-9]+)/;
|
||||
const regExp = /loom\.com\/(?:share|embed)\/([a-zA-Z0-9]+)/;
|
||||
const match = regExp.exec(url);
|
||||
|
||||
if (match?.[1]) {
|
||||
|
||||
@@ -138,5 +138,15 @@ describe("File Input Utils", () => {
|
||||
test("returns false for non-YouTube URLs", () => {
|
||||
expect(checkForYoutubePrivacyMode("https://www.example.com")).toBe(false);
|
||||
});
|
||||
|
||||
test("should return false for empty or whitespace-only string", () => {
|
||||
expect(checkForYoutubePrivacyMode("")).toBe(false);
|
||||
expect(checkForYoutubePrivacyMode(" ")).toBe(false);
|
||||
});
|
||||
|
||||
test("should return false for non-string types", () => {
|
||||
expect(checkForYoutubePrivacyMode(null as any)).toBe(false);
|
||||
expect(checkForYoutubePrivacyMode(123 as any)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -86,6 +86,10 @@ export const getAllowedFiles = async (
|
||||
};
|
||||
|
||||
export const checkForYoutubePrivacyMode = (url: string): boolean => {
|
||||
if (!url || typeof url !== "string" || url.trim() === "") {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsedUrl = new URL(url);
|
||||
return parsedUrl.host === "www.youtube-nocookie.com";
|
||||
|
||||
@@ -202,7 +202,8 @@
|
||||
"xm-and-surveys/xm/best-practices/cancel-subscription",
|
||||
"xm-and-surveys/xm/best-practices/pmf-survey",
|
||||
"xm-and-surveys/xm/best-practices/quiz-time",
|
||||
"xm-and-surveys/xm/best-practices/improve-trial-cr"
|
||||
"xm-and-surveys/xm/best-practices/improve-trial-cr",
|
||||
"xm-and-surveys/xm/best-practices/research-panel"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 59 KiB |
198
docs/xm-and-surveys/xm/best-practices/research-panel.mdx
Normal file
198
docs/xm-and-surveys/xm/best-practices/research-panel.mdx
Normal file
@@ -0,0 +1,198 @@
|
||||
---
|
||||
title: "Panel Management"
|
||||
description: "Build and manage your own research panel using Formbricks to collect profiling data, create targeted segments, and distribute personalized survey links to panel members."
|
||||
icon: "users-rectangle"
|
||||
---
|
||||
|
||||
## Purpose
|
||||
|
||||
A research panel is a pre-recruited group of participants who have agreed to take part in multiple surveys over time. Building your own panel enables you to:
|
||||
|
||||
- Conduct research with a well-understood audience
|
||||
- Target specific demographics or user profiles
|
||||
- Distribute surveys quickly without recruiting new participants each time
|
||||
- Track participant engagement and responses over time
|
||||
|
||||
## Formbricks Approach
|
||||
|
||||
Formbricks provides all the tools you need to build and manage a research panel:
|
||||
|
||||
- **Profiling surveys** to collect participant demographics and preferences
|
||||
- **Contact management** to store and organize panel member data
|
||||
- **Segments** to group participants by attributes for targeted research
|
||||
- **Personal links** to distribute unique survey links to specific panel members
|
||||
|
||||
## Overview
|
||||
|
||||
Building a research panel with Formbricks involves these key steps:
|
||||
|
||||
1. Create a profiling survey to collect panel member information
|
||||
2. Export responses and prepare contact data
|
||||
3. Upload contacts into Formbricks
|
||||
4. Create segments to group panel members
|
||||
5. Create research surveys and distribute personal links
|
||||
|
||||
## Step-by-step guide
|
||||
|
||||
<Steps>
|
||||
<Step title="Create a profiling survey">
|
||||
Start by creating a survey to collect essential information about your panel members. This profiling survey should gather:
|
||||
|
||||
- **Contact information**: Email, name, and any identifiers you need
|
||||
- **Demographics**: Age, location, occupation, etc.
|
||||
- **Preferences**: Product usage, interests, or other relevant attributes
|
||||
|
||||
To create your profiling survey:
|
||||
|
||||
1. Go to [app.formbricks.com](https://app.formbricks.com) and click **Create Survey**
|
||||
2. Choose a blank survey or start from a template
|
||||
3. Add questions to collect the data you need for segmentation
|
||||
4. Include a **Contact Info** question type to capture email addresses
|
||||
|
||||

|
||||
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Collect profiling responses">
|
||||
Distribute your profiling survey and collect responses from potential panel members. You can share the survey via:
|
||||
|
||||
- Direct link
|
||||
- Email
|
||||
- Social media
|
||||
- Your website
|
||||
|
||||
Wait until you have collected a sufficient number of responses to build your panel.
|
||||
</Step>
|
||||
|
||||
<Step title="Download the responses CSV">
|
||||
Once you have collected responses, export them for processing:
|
||||
|
||||
1. Navigate to your profiling survey's **Summary** page
|
||||
2. Click the **Download** button
|
||||
3. Select **CSV** format
|
||||
4. Save the file to your computer
|
||||
|
||||

|
||||
|
||||
This CSV contains all the profiling data from your respondents.
|
||||
</Step>
|
||||
|
||||
<Step title="Download the example contact upload CSV">
|
||||
To understand the required format for uploading contacts, download the example CSV:
|
||||
|
||||
1. Go to the **Contacts** section in your project
|
||||
2. Click **Upload Contacts**
|
||||
3. Download the **Example CSV** to see the required column structure
|
||||
|
||||

|
||||
|
||||
The example CSV shows you the format Formbricks expects for contact uploads, including required fields like email and optional attribute columns.
|
||||
</Step>
|
||||
|
||||
<Step title="Map profiling responses to contact attributes">
|
||||
Transform your profiling survey responses into the contact upload format:
|
||||
|
||||
1. Open both CSV files (responses and example template)
|
||||
2. Create a new spreadsheet matching the upload template structure
|
||||
3. Map each profiling survey question to a contact attribute column:
|
||||
- Email → `email` column
|
||||
- Name → `firstName`, `lastName` columns
|
||||
- Other answers → Custom attribute columns
|
||||
|
||||
Example mapping:
|
||||
|
||||
| Survey Question | Contact Attribute |
|
||||
|----------------|-------------------|
|
||||
| "What is your email?" | `email` |
|
||||
| "What is your name?" | `firstName`, `lastName` |
|
||||
| "What industry do you work in?" | `industry` |
|
||||
| "How large is your company?" | `companySize` |
|
||||
| "What is your job title?" | `jobTitle` |
|
||||
</Step>
|
||||
|
||||
<Step title="Upload contacts to Formbricks">
|
||||
Import your panel members into Formbricks:
|
||||
|
||||
1. Go to the **Contacts** section
|
||||
2. Click **Upload Contacts**
|
||||
3. Select your prepared CSV file
|
||||
4. Review the attribute mapping
|
||||
5. Complete the upload
|
||||
|
||||
Your panel members are now stored in Formbricks with their profiling attributes.
|
||||
</Step>
|
||||
|
||||
<Step title="Create segments for targeting">
|
||||
Group your panel members into segments based on their attributes:
|
||||
|
||||
1. Go to the **Contacts** tab
|
||||
2. Click **Create Segment**
|
||||
|
||||

|
||||
|
||||
3. Define filter conditions based on the attributes you collected:
|
||||
- Example: `industry` equals "Technology" AND `companySize` equals "50-200"
|
||||
|
||||

|
||||
|
||||
4. Name your segment descriptively (e.g., "Tech SMB Professionals")
|
||||
5. Save the segment
|
||||
|
||||
Create multiple segments for different research needs. Learn more about [Advanced Targeting](/xm-and-surveys/surveys/website-app-surveys/advanced-targeting) for detailed segmentation options.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Create a research survey">
|
||||
Now create the survey you want to distribute to your panel:
|
||||
|
||||
1. Click **Create Survey**
|
||||
2. Design your research survey with the questions you need
|
||||
3. Configure survey settings as needed
|
||||
4. Publish the survey
|
||||
</Step>
|
||||
|
||||
<Step title="Generate personal links for your segment">
|
||||
Distribute unique survey links to your panel segment:
|
||||
|
||||
1. Navigate to your research survey's **Summary** page
|
||||
2. Click **Share survey**
|
||||
3. Select the **Personal Links** tab
|
||||
4. Choose the segment you want to survey from the dropdown
|
||||
5. Optionally set an expiry date for the links
|
||||
6. Click **Generate & download links**
|
||||
|
||||

|
||||
|
||||
You'll receive a CSV file containing unique survey links for each panel member in the segment. Learn more about [Personal Links](/xm-and-surveys/surveys/link-surveys/personal-links).
|
||||
</Step>
|
||||
|
||||
<Step title="Distribute your survey">
|
||||
Send the personal links to your panel members using your preferred method:
|
||||
|
||||
- Email marketing platform
|
||||
- Direct email
|
||||
- SMS
|
||||
- Any other communication channel
|
||||
|
||||
Each panel member receives their unique link, and their responses will be automatically attributed to their contact record.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Benefits of this approach
|
||||
|
||||
| Benefit | Description |
|
||||
|---------|-------------|
|
||||
| **Response attribution** | Know exactly who responded to each survey |
|
||||
| **Targeted research** | Survey specific segments without bothering others |
|
||||
| **Panel management** | Maintain a centralized database of research participants |
|
||||
| **Reusability** | Use the same panel for multiple research projects |
|
||||
| **Data enrichment** | Build up participant profiles over time with each survey |
|
||||
|
||||
|
||||
## Next steps
|
||||
|
||||
- [Personal Links](/xm-and-surveys/surveys/link-surveys/personal-links) - Learn more about generating and managing personal survey links
|
||||
- [Advanced Targeting](/xm-and-surveys/surveys/website-app-surveys/advanced-targeting) - Explore detailed segmentation options
|
||||
- [Hidden Fields](/xm-and-surveys/surveys/general-features/hidden-fields) - Pass additional data into surveys via URL parameters
|
||||
|
||||
@@ -3,30 +3,18 @@ import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { checkForLoomUrl, checkForVimeoUrl, checkForYoutubeUrl, convertToEmbedUrl } from "@/lib/video";
|
||||
|
||||
// Function to add extra params to videoUrls in order to reduce video controls
|
||||
const getVideoUrlWithParams = (videoUrl: string): string | undefined => {
|
||||
// First convert to embed URL
|
||||
const embedUrl = convertToEmbedUrl(videoUrl);
|
||||
if (!embedUrl) return undefined;
|
||||
|
||||
//Function to add extra params to videoUrls in order to reduce video controls
|
||||
const getVideoUrlWithParams = (videoUrl: string): string => {
|
||||
const isYoutubeVideo = checkForYoutubeUrl(videoUrl);
|
||||
const isVimeoUrl = checkForVimeoUrl(videoUrl);
|
||||
const isLoomUrl = checkForLoomUrl(videoUrl);
|
||||
|
||||
if (isYoutubeVideo) {
|
||||
// For YouTube, add parameters to embed URL
|
||||
const separator = embedUrl.includes("?") ? "&" : "?";
|
||||
return `${embedUrl}${separator}controls=0`;
|
||||
} else if (isVimeoUrl) {
|
||||
// For Vimeo, add parameters to embed URL
|
||||
const separator = embedUrl.includes("?") ? "&" : "?";
|
||||
return `${embedUrl}${separator}title=false&transcript=false&speed=false&quality_selector=false&progress_bar=false&pip=false&fullscreen=false&cc=false&chromecast=false`;
|
||||
} else if (isLoomUrl) {
|
||||
// For Loom, add parameters to embed URL
|
||||
const separator = embedUrl.includes("?") ? "&" : "?";
|
||||
return `${embedUrl}${separator}hide_share=true&hideEmbedTopBar=true&hide_title=true`;
|
||||
}
|
||||
return embedUrl;
|
||||
if (isYoutubeVideo) return videoUrl.concat("?controls=0");
|
||||
else if (isVimeoUrl)
|
||||
return videoUrl.concat(
|
||||
"?title=false&transcript=false&speed=false&quality_selector=false&progress_bar=false&pip=false&fullscreen=false&cc=false&chromecast=false"
|
||||
);
|
||||
else if (isLoomUrl) return videoUrl.concat("?hide_share=true&hideEmbedTopBar=true&hide_title=true");
|
||||
return videoUrl;
|
||||
};
|
||||
|
||||
interface ElementMediaProps {
|
||||
|
||||
@@ -117,6 +117,16 @@ describe("convertToEmbedUrl", () => {
|
||||
expect(result).toBe("https://player.vimeo.com/video/987654321");
|
||||
});
|
||||
|
||||
test("handles already-embedded Vimeo URLs", () => {
|
||||
const result = convertToEmbedUrl("https://player.vimeo.com/video/123456789");
|
||||
expect(result).toBe("https://player.vimeo.com/video/123456789");
|
||||
});
|
||||
|
||||
test("handles Vimeo URLs with query parameters", () => {
|
||||
const result = convertToEmbedUrl("https://vimeo.com/123456789?some=param");
|
||||
expect(result).toBe("https://player.vimeo.com/video/123456789");
|
||||
});
|
||||
|
||||
test("returns undefined for invalid Vimeo URLs", () => {
|
||||
const result = convertToEmbedUrl("https://www.vimeo.com/invalid");
|
||||
expect(result).toBeUndefined();
|
||||
@@ -134,6 +144,16 @@ describe("convertToEmbedUrl", () => {
|
||||
expect(result).toBe("https://www.loom.com/embed/xyz789");
|
||||
});
|
||||
|
||||
test("handles already-embedded Loom URLs", () => {
|
||||
const result = convertToEmbedUrl("https://www.loom.com/embed/abc123def456");
|
||||
expect(result).toBe("https://www.loom.com/embed/abc123def456");
|
||||
});
|
||||
|
||||
test("handles Loom URLs with query parameters", () => {
|
||||
const result = convertToEmbedUrl("https://www.loom.com/share/abc123def456?some=param");
|
||||
expect(result).toBe("https://www.loom.com/embed/abc123def456");
|
||||
});
|
||||
|
||||
test("returns undefined for invalid Loom URLs", () => {
|
||||
const result = convertToEmbedUrl("https://www.loom.com/invalid");
|
||||
expect(result).toBeUndefined();
|
||||
|
||||
@@ -27,7 +27,7 @@ export const checkForVimeoUrl = (url: string): boolean => {
|
||||
|
||||
if (vimeoUrl.protocol !== "https:") return false;
|
||||
|
||||
const vimeoDomains = ["www.vimeo.com", "vimeo.com"];
|
||||
const vimeoDomains = ["www.vimeo.com", "vimeo.com", "player.vimeo.com"];
|
||||
const hostname = vimeoUrl.hostname;
|
||||
|
||||
return vimeoDomains.includes(hostname);
|
||||
@@ -77,14 +77,14 @@ const extractYoutubeId = (url: string): string | null => {
|
||||
};
|
||||
|
||||
const extractVimeoId = (url: string): string | null => {
|
||||
const regExp = /vimeo\.com\/(?<videoId>\d+)/;
|
||||
const regExp = /vimeo\.com\/(?:video\/)?(?<videoId>\d+)/;
|
||||
const match = regExp.exec(url);
|
||||
|
||||
return match?.groups?.videoId ?? null;
|
||||
};
|
||||
|
||||
const extractLoomId = (url: string): string | null => {
|
||||
const regExp = /loom\.com\/share\/(?<videoId>[a-zA-Z0-9]+)/;
|
||||
const regExp = /loom\.com\/(?:share|embed)\/(?<videoId>[a-zA-Z0-9]+)/;
|
||||
const match = regExp.exec(url);
|
||||
|
||||
return match?.groups?.videoId ?? null;
|
||||
|
||||
@@ -27,7 +27,7 @@ export const checkForVimeoUrl = (url: string): boolean => {
|
||||
|
||||
if (vimeoUrl.protocol !== "https:") return false;
|
||||
|
||||
const vimeoDomains = ["www.vimeo.com", "vimeo.com"];
|
||||
const vimeoDomains = ["www.vimeo.com", "vimeo.com", "player.vimeo.com"];
|
||||
const hostname = vimeoUrl.hostname;
|
||||
|
||||
return vimeoDomains.includes(hostname);
|
||||
@@ -77,7 +77,7 @@ export const extractYoutubeId = (url: string): string | null => {
|
||||
};
|
||||
|
||||
const extractVimeoId = (url: string): string | null => {
|
||||
const regExp = /vimeo\.com\/(\d+)/;
|
||||
const regExp = /vimeo\.com\/(?:video\/)?(\d+)/;
|
||||
const match = url.match(regExp);
|
||||
|
||||
if (match && match[1]) {
|
||||
@@ -87,7 +87,7 @@ const extractVimeoId = (url: string): string | null => {
|
||||
};
|
||||
|
||||
const extractLoomId = (url: string): string | null => {
|
||||
const regExp = /loom\.com\/share\/([a-zA-Z0-9]+)/;
|
||||
const regExp = /loom\.com\/(?:share|embed)\/([a-zA-Z0-9]+)/;
|
||||
const match = url.match(regExp);
|
||||
|
||||
if (match && match[1]) {
|
||||
|
||||
Reference in New Issue
Block a user