Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 12 additions & 31 deletions app/components/UserDataUpload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { GenotypeData, detectAndParseGenotypeFile, validateFileSize, validateFil
import { calculateFileHash } from "@/lib/file-hash";
import {
trackFileCleared,
trackFileUploadError,
trackGenotypeFileLoaded,
trackGenotypeFileUploadFailed,
trackGenotypeFileUploadStarted,
Expand Down Expand Up @@ -38,59 +37,53 @@ export function GenotypeProvider({ children }: { children: React.ReactNode }) {
const [originalFileName, setOriginalFileName] = useState<string | null>(null);

const uploadGenotype = async (file: File, source: string = 'unknown') => {
const startTime = performance.now();
const fileExtension = file.name.split('.').pop() || '';
const dotIdx = file.name.lastIndexOf('.');
const fileExtension = dotIdx !== -1 ? file.name.slice(dotIdx + 1).toLowerCase() : '';

setIsLoading(true);
setError(null);
trackGenotypeFileUploadStarted(source);

try {
// Validate file size (50MB limit)
if (!validateFileSize(file, 50)) {
throw new Error('File too large. Maximum size is 50MB.');
}

// Validate file format
if (!validateFileFormat(file)) {
throw new Error('Invalid file format. Please upload a .txt, .tsv, or .csv file from 23andMe, AncestryDNA, or Monadic DNA.');
throw new Error('Unsupported file type. Please upload a .txt, .tsv, or .csv file exported from 23andMe, AncestryDNA, MyHeritage, FTDNA, LivingDNA, or a compatible provider.');
}

// Read and parse file entirely client-side
const fileContent = await file.text();
const hash = calculateFileHash(fileContent);

// Parse the genotype file client-side
const parseResult = detectAndParseGenotypeFile(fileContent);

if (!parseResult.success) {
throw new Error(parseResult.error || 'Failed to parse genotype data');
const reason = parseResult.error || 'Failed to parse genotype data';
console.error('[Upload] Parse failed', { file: file.name, ext: fileExtension, reason });
throw new Error(reason);
}

// Create a map for quick SNP lookup
const genotypeMap = new Map<string, string>();
parseResult.data!.forEach((variant: GenotypeData) => {
genotypeMap.set(variant.rsid, variant.genotype);
});

const parseDuration = performance.now() - startTime;

// Track successful genotype file load
trackGenotypeFileLoaded(file.size, genotypeMap.size, source);
trackGenotypeFileLoaded(file.size, genotypeMap.size, source, parseResult.detectedFormat, fileExtension);

setGenotypeData(genotypeMap);
setFileHash(hash);
setOriginalFileName(file.name);

// Call the callback if it exists
if (onDataLoadedRef.current) {
onDataLoadedRef.current();
}
return true;
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Upload failed';
console.error('[Upload] Failed', { file: file.name, ext: fileExtension, source, reason: errorMessage });
setError(errorMessage);
trackGenotypeFileUploadFailed(source, errorMessage);
trackGenotypeFileUploadFailed(source, errorMessage, fileExtension);
return false;
} finally {
setIsLoading(false);
Expand Down Expand Up @@ -159,19 +152,6 @@ export default function UserDataUpload() {
const file = event.target.files?.[0];
if (!file) return;

// Validate file type
const fileName = file.name.toLowerCase();
if (!fileName.endsWith('.txt') && !fileName.endsWith('.tsv') && !fileName.endsWith('.csv')) {
trackFileUploadError('unsupported_file_type');
return;
}

// Validate file size (50MB limit)
if (file.size > 50 * 1024 * 1024) {
trackFileUploadError('file_too_large');
return;
}

// Dev mode: Try to use File System Access API to save handle for future auto-load
if (isDevModeEnabled()) {
try {
Expand Down Expand Up @@ -228,9 +208,10 @@ export default function UserDataUpload() {
<label htmlFor="genotype-upload" className={`genotype-upload-label ${isLoading ? 'loading' : ''}`}>
{isLoading ? 'Analyzing your genetic map...' : 'Choose File to Upload'}
</label>
<p className="upload-format-hint">23andMe, AncestryDNA, MyHeritage, FTDNA, LivingDNA, and more</p>
{error && (
<div className="genotype-error" title={error}>
Upload failed
<div className="genotype-error">
{error}
</div>
)}
<div className="sample-data-section">
Expand Down
7 changes: 7 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -1857,6 +1857,13 @@ tbody tr:hover {
cursor: wait;
}

.upload-format-hint {
margin: 0.3rem 0 0;
font-size: 0.75rem;
color: var(--text-secondary);
opacity: 0.8;
}

.sample-data-section {
display: flex;
flex-direction: column;
Expand Down
7 changes: 5 additions & 2 deletions lib/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,18 +327,21 @@ export function trackGenotypeFileUploadStarted(source: string = 'unknown') {
});
}

export function trackGenotypeFileUploadFailed(source: string = 'unknown', reason?: string) {
export function trackGenotypeFileUploadFailed(source: string = 'unknown', reason?: string, fileExtension?: string) {
trackEvent('genotype_file_upload_failed', {
source,
reason: sanitizeErrorReason(reason),
...(fileExtension && { file_extension: fileExtension }),
});
}

export function trackGenotypeFileLoaded(fileSize: number, variantCount: number, source: string = 'unknown') {
export function trackGenotypeFileLoaded(fileSize: number, variantCount: number, source: string = 'unknown', detectedFormat?: string, fileExtension?: string) {
const metadata = {
file_size_kb: Math.round(fileSize / 1024),
variant_count: variantCount,
source,
...(detectedFormat && { detected_format: detectedFormat }),
...(fileExtension && { file_extension: fileExtension }),
};

trackEvent('genotype_file_loaded', metadata);
Expand Down
Loading
Loading