Merge pull request #1042 from buster-so/big-nate-bus-1819-backslash-being-added-before-metricid-in-reports

Big nate bus 1819 backslash being added before metricid in reports
This commit is contained in:
Nate Kelley 2025-09-22 15:10:18 -06:00 committed by GitHub
commit ddde4343d0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 41 additions and 7 deletions

View File

@ -18,3 +18,26 @@ export const unescapeHtmlAttribute = (str: string): string => {
.replace(/>/g, '>')
.replace(/&/g, '&'); // Must be last to avoid double-decoding
};
// Pre-process markdown to handle escape sequences and special characters
export const preprocessMarkdownForMdx = (markdown: string): string => {
// First, convert literal escape sequences to actual characters
// This handles cases where markdown is passed as a string literal with escaped characters
let processed = markdown
.replace(/\\n/g, '\n') // Convert \n to actual newlines
.replace(/\\'/g, "'") // Convert \' to apostrophes
.replace(/\\"/g, '"'); // Convert \" to quotes
// Then escape < symbols that are not part of HTML tags or components
// This prevents them from being interpreted as unclosed tags by the MDX parser
// Pattern explanation:
// - Matches < that is NOT followed by:
// - A letter (start of HTML tag like <div)
// - A slash (closing tag like </div)
// - An uppercase letter (React component like <Metric)
// - metric (our custom metric tags)
// - But IS part of text content (has word boundary or number after it)
processed = processed.replace(/<(?![a-zA-Z/]|metric\s)/g, '\\<');
return processed;
};

View File

@ -1864,4 +1864,16 @@ More streamed content.`;
expect(currentContent).not.toContain('\\<metric');
expect(currentContent).toContain('<metric metricId="streamed-metric"');
});
it('should handle problematic content', async () => {
const markdown = `This analysis reveals two dramatically different customer universes within our business. Of our 19,119 total customers, **31 elite customers (0.16%) generate >500k CLV** while **19,088 customers (99.84%) have <500k CLV**. The >500k CLV segment represents serious cycling enthusiasts who make large in-store purchases averaging $66,232 per order, while the <500k CLV segment consists of casual recreational cyclists making smaller online purchases averaging $2,863 per order. These segments exhibit completely different behavioral profiles, geographic concentrations, and product preferences, suggesting they require entirely different marketing and service strategies.\n\n## Customer Segment Overview\n\n<metric metricId="be286e99-77f9-4b6e-959c-c2691d2d549e"/>\n\nThe data reveals a stark divide in our customer base. The **>500k CLV segment averages $666,590 per customer** compared to just **$4,672 for the <500k CLV segment** - a 143x difference. Despite representing only 0.16% of customers, the elite >500k CLV segment contributes **$20.7 million in total lifetime value**.\n\n## Customer Behavior Profiles Show Completely Different Cycling Enthusiasts\n\n<metric metricId="ed9c70f3-619b-40ee-a99d-c6dfa2945bb8"/>\n\nThe behavioral analysis reveals two entirely different customer types. **97% of >500k CLV customers are daily cyclists with advanced technical knowledge**, representing serious cycling enthusiasts. In stark contrast, the <500k CLV segment is dominated by **occasional cyclists with basic technical knowledge** (6,697 customers) and **monthly cyclists with intermediate knowledge** (5,493 customers). This suggests the high-value customers are passionate cyclists who view cycling as a serious pursuit, while the majority are casual recreational users.\n\n## Geographic Concentration Reveals Strategic Opportunities\n\n<metric metricId="0219343f-95c6-4a04-bbd9-7e052867abd9"/>\n\nThe >500k CLV customers show significant geographic concentration, with **Southwest (9 customers) and Northwest (7 customers) territories accounting for 52% of elite customers**. This contrasts sharply with the <500k CLV segment's broader global distribution, including strong presence in Australia (3,625 customers). The concentration of high-value customers in specific US regions suggests targeted relationship management and premium service opportunities in these key markets.\n\n## Order Behavior Reveals Dramatically Different Purchase Patterns\n\n<metric metricId="4e276567-45ae-4618-9176-9173c9535464"/>\n\nThe purchase behavior differences are striking. **>500k CLV customers average $66,232 per order and place 10.1 orders per customer**, indicating they make substantial, repeat purchases. Meanwhile, **<500k CLV customers average just $2,863 per order with only 1.6 orders per customer**, suggesting primarily one-time or infrequent purchases. This 23x difference in order value demonstrates that elite customers are making major cycling investments rather than casual purchases.\n\n## Product Category Preferences Show Elite Focus on Premium Equipment\n\n<metric metricId="83756125-45fe-4a38-a4c0-9badc0abc458"/>\n\nBoth segments prioritize bikes, but with different spending patterns. **>500k CLV customers spend 83% of their budget on bikes ($17.1M) and 15% on components ($3.1M)**, indicating serious cyclists investing in high-end equipment and performance upgrades. The <500k CLV segment also focuses on bikes ($77.6M) but with much lower per-customer spending. The elite segment's heavy component spending suggests they're upgrading and customizing their bikes extensively.\n\n## Sales Channel Preferences Highlight Service Expectations\n\n<metric metricId="654aa8cb-d2e0-4b1d-812a-1ab94182c656"/>\n\n**100% of >500k CLV customers purchase exclusively in-store**, demonstrating their preference for personal service, expert consultation, and hands-on product evaluation. This contrasts dramatically with <500k CLV customers, who make **89% of their purchases online** (27,659 orders) for convenience and price comparison. The elite segment's in-store preference suggests they value relationship-based selling and technical expertise, making them ideal candidates for premium service programs and dedicated account management.\n\n## Product Preferences Reveal Different Quality Tiers\n\n<metric metricId="dd0e7ca4-e3f7-4842-8459-7ad588e9b81a"/>\n\nThe product preferences show interesting patterns. **>500k CLV customers favor Road-250 models** (particularly Road-250 Black, 44 with $780K revenue), indicating preference for road cycling and premium models. Meanwhile, **<500k CLV customers predominantly choose Mountain-200 series bikes**, with Mountain-200 Black, 38 generating $3.7M in total revenue across 1,162 orders. The elite segment's focus on Road-250 models suggests they're serious road cyclists, while the broader market prefers versatile mountain bikes.`;
const platejs = await markdownToPlatejs(editor, markdown);
// Test passes if no error is thrown - the markdown should be fully parsed
expect(platejs).toBeDefined();
expect(platejs.length).toBeGreaterThan(10); // Should have many elements, not just the first paragraph
expect(platejs[1].type).toBe('h2');
expect(platejs[2].type).toBe('metric');
expect(platejs[2].metricId).toBe('be286e99-77f9-4b6e-959c-c2691d2d549e');
expect(platejs[3].type).toBe('p');
});
});

View File

@ -1,5 +1,6 @@
import type { Descendant, TElement, Value } from 'platejs';
import type { Descendant, Value } from 'platejs';
import type { IReportEditor } from '../../ReportEditor';
import { preprocessMarkdownForMdx } from './escape-handlers';
import { postProcessToggleDeserialization, postProcessToggleMarkdown } from './toggle-serializer';
export const markdownToPlatejs = async (
@ -7,16 +8,14 @@ export const markdownToPlatejs = async (
markdown: string
): Promise<Value> => {
try {
const descendants: Value = editor.api.markdown.deserialize(markdown);
// Pre-process markdown to escape < symbols that aren't part of HTML tags
const processedMarkdown = preprocessMarkdownForMdx(markdown);
const descendants: Value = editor.api.markdown.deserialize(processedMarkdown);
const descendantsWithIds: Value = descendants.map((element, index) => ({
...element,
id: `id-${index}`,
}));
// Apply post-processing to handle details elements
const processedElements = postProcessToggleDeserialization(descendantsWithIds as TElement[]);
return processedElements as Value;
return postProcessToggleDeserialization(descendantsWithIds);
} catch (error) {
console.error('Error converting markdown to PlateJS:', error);
return [];