package main import ( "encoding/json" "net/http" "os" "path/filepath" "regexp" "strings" ) type mraidIssue struct { ID string `json:"id"` Severity string `json:"severity"` Title string `json:"title"` Detail string `json:"detail"` Line int `json:"line,omitempty"` Reference string `json:"reference"` } type mraidResult struct { File string `json:"file"` OK bool `json:"ok"` Issues []mraidIssue `json:"issues"` } type mraidRule struct { ID string Severity string Title string Detail string Reference string Test func(mraidContext, mraidRule) *mraidIssue } type mraidContext struct { FilePath string HTML string ScriptText string MraidCallLines map[string]int } func MraidPage(w http.ResponseWriter, r *http.Request) { body := `

MRAID Checker

Static checks for AppLovin, ironSource, and Unity HTML builds based on GUIDE.md plus the MRAID 3.0 specification and Best Practices Guide.

Inputs

or drop HTML files or folders here
` w.Header().Set("Content-Type", "text/html; charset=utf-8") _, _ = w.Write([]byte(Page("/mraid", "MRAID Checker", body))) } func MraidPickFolder(w http.ResponseWriter, r *http.Request) { p := PickFolder("Select folder to scan", "") if p == "" { writeJSON(w, map[string]any{"paths": []string{}}) return } files := []string{} for _, f := range collectHTMLFiles(p) { if isInSupportedMraidFolderPath(f) { files = append(files, f) } } writeJSON(w, map[string]any{"paths": files}) } func MraidPickFiles(w http.ResponseWriter, r *http.Request) { paths := PickFiles("Select HTML files", []FileFilter{{Name: "HTML", Extensions: []string{"html", "htm"}}}, true, "") writeJSON(w, map[string]any{"paths": paths}) } func MraidScan(w http.ResponseWriter, r *http.Request) { var req struct { Folder string `json:"folder"` Files []string `json:"files"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, map[string]any{"results": []mraidResult{}, "error": err.Error()}) return } var targets []string var baseDir string skipped := 0 skippedReason := "" noTargetsError := "No HTML files were found." if len(req.Files) > 0 { var existing []string for _, f := range req.Files { if fileExists(f) { existing = append(existing, f) } } targets = existing skipped = len(req.Files) - len(existing) skippedReason = "missing file(s)." noTargetsError = "No selected HTML files were found." if len(targets) > 0 { baseDir = commonBaseDir(targets) } } else if req.Folder != "" && fileExists(req.Folder) { all := collectHTMLFiles(req.Folder) for _, f := range all { if isInSupportedMraidFolderPath(f) { targets = append(targets, f) } } skipped = len(all) - len(targets) skippedReason = "HTML file(s) outside AppLovin/ironSource/Unity folders." noTargetsError = "No AppLovin, ironSource, or Unity folder HTML files were found." baseDir = req.Folder } else { writeJSON(w, map[string]any{"results": []mraidResult{}, "error": "Pick a folder or files first"}) return } if len(targets) == 0 { writeJSON(w, map[string]any{"results": []mraidResult{}, "skipped": skipped, "skippedReason": skippedReason, "error": noTargetsError}) return } results := []mraidResult{} for _, file := range targets { data, err := os.ReadFile(file) if err != nil { continue } issues := checkMraid(string(data), file) display := file if baseDir != "" { if rel, err := filepath.Rel(baseDir, file); err == nil && rel != "" { display = rel } else { display = filepath.Base(file) } } results = append(results, mraidResult{File: display, OK: len(issues) == 0, Issues: issues}) } writeJSON(w, map[string]any{"results": results, "skipped": skipped, "skippedReason": skippedReason}) } var ( mraidScriptRx = regexp.MustCompile(`(?is)]*>(.*?)`) mraidCallRx = regexp.MustCompile(`(?i)\bmraid\s*\.\s*([a-zA-Z_$][\w$]*)\s*\(`) mraidReferenceRx = regexp.MustCompile(`(?i)\bmraid\b`) mraidJsRx = regexp.MustCompile(`(?i)\bmraid\.js\b`) mraidViewportRx = regexp.MustCompile(`(?i)]*\bname\s*=\s*['"]viewport['"][^>]*>`) mraidWindowOpenRx = regexp.MustCompile(`(?i)\bwindow\s*\.\s*open\s*\(`) mraidLocationNavRx = regexp.MustCompile(`(?i)\blocation\s*\.\s*(href|assign|replace)\b`) mraidHyperlinkRx = regexp.MustCompile(`(?i)]*\bhref\s*=`) mraidCloseUiRx = regexp.MustCompile(`(?i)\b(close|dismiss|skip)\b`) mraidLoadingLeftRx = regexp.MustCompile(`(?i)mraid\s*\.\s*getState\s*\(\s*\)\s*={0,2}\s*['"]loading['"]`) mraidLoadingRightRx = regexp.MustCompile(`(?i)['"]loading['"]\s*={0,2}\s*mraid\s*\.\s*getState\s*\(\s*\)`) mraidDomReadyRx = regexp.MustCompile(`(?i)document\s*\.\s*readyState|DOMContentLoaded|\bwindow\s*\.\s*(addEventListener\s*\(\s*['"]load['"]|onload\b)`) mraidTypeofGuardRx = regexp.MustCompile(`(?i)typeof\s+(window\s*\.\s*)?mraid\s*!={1,2}\s*['"]undefined['"]`) mraidReadyTimeoutRx = regexp.MustCompile(`(?is)setTimeout\s*\(.*mraid|mraid.*setTimeout\s*\(`) mraidLateDetectionRx = regexp.MustCompile(`(?i)(setInterval\s*\(|setTimeout\s*\(|requestAnimationFrame\s*\().*(window\s*\.\s*mraid|\bmraid\b)|(window\s*\.\s*mraid|\bmraid\b).*(setInterval\s*\(|setTimeout\s*\(|requestAnimationFrame\s*\()`) mraidOpenGuardRx = regexp.MustCompile(`(?i)typeof\s+(window\s*\.\s*)?mraid\s*\.\s*open\s*={2,3}\s*['"]function['"]|typeof\s+(window\s*\.\s*)?mraid\s*\.\s*open\s*!={1,2}\s*['"]undefined['"]`) mraidOpenLoadingGuardRx = regexp.MustCompile(`(?i)mraid\s*\.\s*getState\s*\(\s*\)\s*!={1,2}\s*['"]loading['"]|['"]loading['"]\s*!={1,2}\s*mraid\s*\.\s*getState\s*\(\s*\)|typeof\s+(window\s*\.\s*)?mraid\s*\.\s*getState\s*!={1,2}\s*['"]function['"]`) mraidVariableLoadingGuardRx = regexp.MustCompile(`(?i)\b\w+\s*!={1,2}\s*['"]loading['"]|['"]loading['"]\s*!={1,2}\s*\w+\b`) mraidLoadingFallbackRx = regexp.MustCompile(`(?is)\b\w+\s*={2,3}\s*['"]loading['"].{0,500}\bwindow\s*\.\s*open\s*\(`) mraidAudioSafeRx = regexp.MustCompile(`(?i)typeof\s+\w+\s*={2,3}\s*['"]number['"]|!=\s*null|!==\s*null`) mraidTwoPartExpandRx = regexp.MustCompile(`(?i)mraid\s*\.\s*expand\s*\(\s*[^)\s]`) mraidVideoOpenRx = regexp.MustCompile(`(?i)mraid\s*\.\s*open\s*\(\s*[^)]*\.(mp4|m4v|mov|webm|avi|m3u8)(['"` + "`" + `?#)]|$)`) mraidRejectedScriptAttrsRx = regexp.MustCompile(`(?i)]*(\btype\s*=\s*['"]module['"]|\bcrossorigin\b)`) mraidConsoleErrorRx = regexp.MustCompile(`(?i)\bconsole\s*\.\s*error\s*\(`) ) const mraidReference = "GUIDE.md, MRAID 3.0 specification, and MRAID 3.0 Best Practices Guide." func isInSupportedMraidFolderPath(filePath string) bool { supported := map[string]bool{ "al": true, "applovin": true, "app-lovin": true, "is": true, "ironsource": true, "iron-source": true, "iron_source": true, "un": true, "unity": true, "unityads": true, "unity-ads": true, "unity_ads": true, } parts := strings.FieldsFunc(strings.ToLower(filepath.Dir(filePath)), func(r rune) bool { return r == '/' || r == '\\' }) for _, part := range parts { if supported[part] { return true } } return false } func checkMraid(htmlSrc string, filePath ...string) []mraidIssue { sourcePath := "" if len(filePath) > 0 { sourcePath = filePath[0] } ctx := mraidContext{ FilePath: sourcePath, HTML: htmlSrc, ScriptText: extractMraidScriptText(htmlSrc), MraidCallLines: collectMraidCallLines(htmlSrc), } var out []mraidIssue for _, rule := range mraidRules { if found := rule.Test(ctx, rule); found != nil { out = append(out, *found) } } if out == nil { out = []mraidIssue{} } return out } func extractMraidScriptText(htmlSrc string) string { var parts []string for _, m := range mraidScriptRx.FindAllStringSubmatch(htmlSrc, -1) { parts = append(parts, m[1]) } return strings.Join(parts, "\n") } func collectMraidCallLines(htmlSrc string) map[string]int { out := map[string]int{} for idx, line := range strings.Split(htmlSrc, "\n") { for _, m := range mraidCallRx.FindAllStringSubmatch(line, -1) { if _, ok := out[m[1]]; !ok { out[m[1]] = idx + 1 } } } return out } func mraidHasReference(ctx mraidContext) bool { return mraidReferenceRx.MatchString(ctx.HTML) } func mraidCountJSReferences(ctx mraidContext) int { return len(mraidJsRx.FindAllStringIndex(ctx.HTML, -1)) } func mraidHasViewport(ctx mraidContext) bool { return mraidViewportRx.MatchString(ctx.HTML) } func mraidIsNetworkTarget(ctx mraidContext) bool { if ctx.FilePath == "" { return true } name := strings.ToLower(filepath.Base(ctx.FilePath)) return isInSupportedMraidFolderPath(ctx.FilePath) || strings.HasSuffix(name, "_al.html") || strings.HasSuffix(name, "_is.html") || strings.HasSuffix(name, "_un.html") || strings.HasSuffix(name, "_al.htm") || strings.HasSuffix(name, "_is.htm") || strings.HasSuffix(name, "_un.htm") } func mraidHasCall(ctx mraidContext, method string) bool { _, ok := ctx.MraidCallLines[method] return ok } func mraidLine(ctx mraidContext, method string) int { return ctx.MraidCallLines[method] } func mraidHasFunctionGuard(ctx mraidContext, method string) bool { escaped := regexp.QuoteMeta(method) rx := regexp.MustCompile(`(?i)typeof\s+(window\s*\.\s*)?mraid\s*\.\s*` + escaped + `\s*={2,3}\s*['"]function['"]|typeof\s+(window\s*\.\s*)?mraid\s*\.\s*` + escaped + `\s*!={1,2}\s*['"]undefined['"]`) return rx.MatchString(ctx.ScriptText) } func mraidHasEventListener(ctx mraidContext, eventName string) bool { rx := regexp.MustCompile(`(?i)mraid\s*\.\s*addEventListener\s*\(\s*['"]` + regexp.QuoteMeta(eventName) + `['"]`) return rx.MatchString(ctx.ScriptText) } func mraidHasSupportCheck(ctx mraidContext, feature string) bool { rx := regexp.MustCompile(`(?i)mraid\s*\.\s*supports\s*\(\s*['"]` + regexp.QuoteMeta(feature) + `['"]`) return rx.MatchString(ctx.ScriptText) } func mraidHasReadyGate(ctx mraidContext) bool { return mraidHasEventListener(ctx, "ready") || mraidLoadingLeftRx.MatchString(ctx.ScriptText) || mraidLoadingRightRx.MatchString(ctx.ScriptText) } func mraidHasDomReadyGate(ctx mraidContext) bool { return mraidDomReadyRx.MatchString(ctx.ScriptText) } func mraidHasReadyStateFallback(ctx mraidContext) bool { return mraidHasEventListener(ctx, "ready") && mraidHasCall(ctx, "getState") } func mraidHasTypeofGuard(ctx mraidContext) bool { return mraidTypeofGuardRx.MatchString(ctx.ScriptText) } func mraidHasReadyTimeout(ctx mraidContext) bool { return mraidReadyTimeoutRx.MatchString(ctx.ScriptText) } func mraidHasLateDetection(ctx mraidContext) bool { return mraidLateDetectionRx.MatchString(ctx.ScriptText) } func mraidHasOpenGuard(ctx mraidContext) bool { return mraidOpenGuardRx.MatchString(ctx.ScriptText) } func mraidHasOpenLoadingGuard(ctx mraidContext) bool { return mraidOpenLoadingGuardRx.MatchString(ctx.ScriptText) || (mraidHasCall(ctx, "getState") && mraidVariableLoadingGuardRx.MatchString(ctx.ScriptText)) || (mraidHasCall(ctx, "getState") && mraidLoadingFallbackRx.MatchString(ctx.ScriptText)) } func mraidUsesBrowserNavigation(ctx mraidContext) bool { return mraidWindowOpenRx.MatchString(ctx.ScriptText) } func mraidUsesLocationNavigation(ctx mraidContext) bool { return mraidLocationNavRx.MatchString(ctx.ScriptText) } func mraidHasAudioVolumeSafeHandling(ctx mraidContext) bool { return mraidHasEventListener(ctx, "audioVolumeChange") && mraidAudioSafeRx.MatchString(ctx.ScriptText) } func mraidHasLifecycleSignal(ctx mraidContext, name string) bool { return regexp.MustCompile(`(?i)\b` + regexp.QuoteMeta(name) + `\b`).MatchString(ctx.HTML) } func mraidIssueFrom(rule mraidRule, line int) *mraidIssue { found := mraidIssue{ID: rule.ID, Severity: rule.Severity, Title: rule.Title, Detail: rule.Detail, Reference: rule.Reference} if line > 0 { found.Line = line } return &found } var mraidRules = []mraidRule{ {ID: "mraid-js-missing", Severity: "requirement", Title: "mraid.js reference not found", Detail: "MRAID creatives for AppLovin, ironSource, and Unity must request mraid.js early in the generated HTML.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue { if mraidIsNetworkTarget(ctx) && mraidCountJSReferences(ctx) == 0 { return mraidIssueFrom(rule, 0) } return nil }}, {ID: "mraid-js-duplicate", Severity: "requirement", Title: "Multiple mraid.js references found", Detail: "Request mraid.js exactly once; duplicate references can inject multiple MRAID libraries and slow or destabilize the ad.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue { if mraidCountJSReferences(ctx) > 1 { return mraidIssueFrom(rule, 0) } return nil }}, {ID: "mraid-reference", Severity: "requirement", Title: "MRAID API reference not found", Detail: "Playable HTML should use the injected mraid object when it depends on an MRAID container.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue { if mraidHasReference(ctx) { return nil } return mraidIssueFrom(rule, 0) }}, {ID: "viewport-meta", Severity: "best-practice", Title: "Viewport meta tag not found", Detail: "MRAID creatives should include a mobile viewport meta tag so the ad scales predictably on mobile WebViews.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue { if mraidHasViewport(ctx) { return nil } return mraidIssueFrom(rule, 0) }}, {ID: "mraid-typeof-guard", Severity: "requirement", Title: "MRAID object is not guarded", Detail: "Guard MRAID access with typeof mraid !== \"undefined\" or an equivalent window.mraid guard before making MRAID calls.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue { if mraidHasReference(ctx) && !mraidHasTypeofGuard(ctx) { return mraidIssueFrom(rule, 0) } return nil }}, {ID: "dom-ready-gate", Severity: "requirement", Title: "DOM readiness is not guarded", Detail: "MRAID startup should wait for DOM readiness as well as MRAID readiness before building or binding rich media content.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue { if !mraidHasReference(ctx) || mraidHasDomReadyGate(ctx) { return nil } return mraidIssueFrom(rule, 0) }}, {ID: "ready-gate", Severity: "requirement", Title: "MRAID ready state is not guarded", Detail: "Gate ad startup behind mraid.ready, or call startup immediately only when mraid.getState() is not loading.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue { if !mraidHasReference(ctx) || mraidHasReadyGate(ctx) { return nil } return mraidIssueFrom(rule, 0) }}, {ID: "ready-state-fallback", Severity: "requirement", Title: "MRAID ready listener/state fallback pair not found", Detail: "Use both mraid.addEventListener(\"ready\", ...) and mraid.getState() so startup works when ready fires before the listener attaches.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue { if !mraidHasReference(ctx) || mraidHasReadyStateFallback(ctx) { return nil } return mraidIssueFrom(rule, 0) }}, {ID: "ready-timeout-fallback", Severity: "best-practice", Title: "MRAID ready timeout fallback not found", Detail: "A timeout fallback prevents startup hangs when a container never fires ready.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue { if !mraidHasReference(ctx) || mraidHasReadyTimeout(ctx) { return nil } return mraidIssueFrom(rule, 0) }}, {ID: "late-mraid-detection", Severity: "best-practice", Title: "Late MRAID injection detection not found", Detail: "Poll or retry briefly for late window.mraid injection before deciding the creative is running without MRAID.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue { if !mraidHasReference(ctx) || mraidHasLateDetection(ctx) { return nil } return mraidIssueFrom(rule, 0) }}, {ID: "error-listener", Severity: "best-practice", Title: "No MRAID error listener found", Detail: "Listen for mraid error events so unsupported or failed MRAID calls can be logged or handled gracefully.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue { if !mraidHasReference(ctx) || mraidHasEventListener(ctx, "error") { return nil } return mraidIssueFrom(rule, 0) }}, {ID: "viewability", Severity: "best-practice", Title: "MRAID viewability fallback chain not found", Detail: "Use exposureChange for MRAID 3.0 and keep viewableChange or isViewable() as MRAID 2.0 fallback before starting animation, audio, video, or timers.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue { hasVisibility := mraidHasEventListener(ctx, "exposureChange") && (mraidHasCall(ctx, "isViewable") || mraidHasEventListener(ctx, "viewableChange")) if !mraidHasReference(ctx) || hasVisibility { return nil } return mraidIssueFrom(rule, 0) }}, {ID: "audio-volume-change", Severity: "best-practice", Title: "audioVolumeChange handling not found", Detail: "Listen for audioVolumeChange and ignore null values; only apply volume math when the value is numeric.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue { if !mraidHasReference(ctx) || mraidHasAudioVolumeSafeHandling(ctx) { return nil } return mraidIssueFrom(rule, 0) }}, {ID: "open-api", Severity: "requirement", Title: "Browser navigation used instead of mraid.open", Detail: "Use mraid.open(url) for click-through navigation inside an MRAID ad container; avoid hyperlinks, location redirects, and unguarded browser navigation.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue { if mraidHyperlinkRx.MatchString(ctx.HTML) || mraidUsesLocationNavigation(ctx) || (mraidUsesBrowserNavigation(ctx) && !mraidHasCall(ctx, "open")) { return mraidIssueFrom(rule, 0) } return nil }}, {ID: "open-guarded-fallback", Severity: "requirement", Title: "mraid.open guarded fallback not found", Detail: "Guard mraid.open with function/state checks and provide a window.open fallback when MRAID is unavailable or still loading.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue { if mraidHasCall(ctx, "open") && (!mraidHasOpenGuard(ctx) || !mraidHasOpenLoadingGuard(ctx) || !mraidUsesBrowserNavigation(ctx)) { return mraidIssueFrom(rule, mraidLine(ctx, "open")) } return nil }}, {ID: "open-video-url", Severity: "best-practice", Title: "mraid.open appears to target video media", Detail: "Use mraid.playVideo(url) for native video playback cases instead of mraid.open(videoUrl).", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue { if mraidVideoOpenRx.MatchString(ctx.ScriptText) && !mraidHasCall(ctx, "playVideo") { return mraidIssueFrom(rule, mraidLine(ctx, "open")) } return nil }}, {ID: "close-api", Severity: "best-practice", Title: "No mraid.close call found", Detail: "If the creative renders its own close control, wire that control to mraid.close().", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue { if mraidCloseUiRx.MatchString(ctx.HTML) && !mraidHasCall(ctx, "close") { return mraidIssueFrom(rule, 0) } return nil }}, {ID: "unload-fallback", Severity: "best-practice", Title: "No mraid.unload graceful failure path found", Detail: "Use mraid.unload() when the ad cannot run or refuses to show, instead of leaving a broken or blank creative.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue { if mraidHasReference(ctx) && !mraidHasCall(ctx, "unload") { return mraidIssueFrom(rule, 0) } return nil }}, {ID: "resize-properties", Severity: "requirement", Title: "mraid.resize used without setResizeProperties", Detail: "Call mraid.setResizeProperties(...) before mraid.resize(); otherwise the container should emit an error.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue { if mraidHasCall(ctx, "resize") && !mraidHasCall(ctx, "setResizeProperties") { return mraidIssueFrom(rule, mraidLine(ctx, "resize")) } return nil }}, {ID: "use-custom-close", Severity: "best-practice", Title: "Deprecated useCustomClose found", Detail: "MRAID 3.0 ignores useCustomClose; the host provides the mandatory close control.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue { if mraidHasCall(ctx, "useCustomClose") || regexp.MustCompile(`(?i)useCustomClose`).MatchString(ctx.ScriptText) { return mraidIssueFrom(rule, mraidLine(ctx, "useCustomClose")) } return nil }}, {ID: "two-part-expand", Severity: "best-practice", Title: "Two-part expand appears to be used", Detail: "Two-part expandable ads are deprecated in MRAID 3.0; use self-contained one-part expandables.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue { if mraidTwoPartExpandRx.MatchString(ctx.ScriptText) { return mraidIssueFrom(rule, mraidLine(ctx, "expand")) } return nil }}, {ID: "expand-support", Severity: "requirement", Title: "mraid.expand used without function guard", Detail: "Guard mraid.expand with a function check before using expand behavior.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue { if mraidHasCall(ctx, "expand") && !mraidHasFunctionGuard(ctx, "expand") { return mraidIssueFrom(rule, mraidLine(ctx, "expand")) } return nil }}, {ID: "resize-support", Severity: "requirement", Title: "mraid.resize used without function guard", Detail: "Guard mraid.resize with a function check before using resize behavior.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue { if mraidHasCall(ctx, "resize") && !mraidHasFunctionGuard(ctx, "resize") { return mraidIssueFrom(rule, mraidLine(ctx, "resize")) } return nil }}, {ID: "store-picture-support", Severity: "requirement", Title: "storePicture used without supports check", Detail: "Call mraid.supports(\"storePicture\") before mraid.storePicture(...).", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue { if mraidHasCall(ctx, "storePicture") && !mraidHasSupportCheck(ctx, "storePicture") { return mraidIssueFrom(rule, mraidLine(ctx, "storePicture")) } return nil }}, {ID: "calendar-support", Severity: "requirement", Title: "createCalendarEvent used without supports check", Detail: "Call mraid.supports(\"calendar\") before mraid.createCalendarEvent(...).", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue { if mraidHasCall(ctx, "createCalendarEvent") && !mraidHasSupportCheck(ctx, "calendar") { return mraidIssueFrom(rule, mraidLine(ctx, "createCalendarEvent")) } return nil }}, {ID: "location-support", Severity: "requirement", Title: "getLocation used without supports check", Detail: "Call mraid.supports(\"location\") before mraid.getLocation(). Do not use HTML5 geolocation as a substitute in MRAID.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue { if mraidHasCall(ctx, "getLocation") && !mraidHasSupportCheck(ctx, "location") { return mraidIssueFrom(rule, mraidLine(ctx, "getLocation")) } return nil }}, {ID: "vpaid-support", Severity: "requirement", Title: "VPAID integration used without supports check", Detail: "Call mraid.supports(\"vpaid\") before mraid.initVpaid(...).", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue { if mraidHasCall(ctx, "initVpaid") && !mraidHasSupportCheck(ctx, "vpaid") { return mraidIssueFrom(rule, mraidLine(ctx, "initVpaid")) } return nil }}, {ID: "orientation-support", Severity: "best-practice", Title: "Orientation properties set without function guard", Detail: "Guard mraid.setOrientationProperties with a function check before requiring orientation behavior.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue { if mraidHasCall(ctx, "setOrientationProperties") && !mraidHasFunctionGuard(ctx, "setOrientationProperties") { return mraidIssueFrom(rule, mraidLine(ctx, "setOrientationProperties")) } return nil }}, {ID: "size-change", Severity: "best-practice", Title: "No sizeChange listener found", Detail: "Listen for sizeChange when layout depends on container size, resize, expand, or orientation behavior.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue { depends := mraidHasCall(ctx, "getMaxSize") || mraidHasCall(ctx, "getScreenSize") || mraidHasCall(ctx, "resize") || mraidHasCall(ctx, "expand") || mraidHasCall(ctx, "setOrientationProperties") if depends && !mraidHasEventListener(ctx, "sizeChange") { return mraidIssueFrom(rule, 0) } return nil }}, {ID: "state-change", Severity: "best-practice", Title: "No stateChange listener found", Detail: "Listen for stateChange when using expand, resize, close, or other state-changing container behavior.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue { changes := mraidHasCall(ctx, "expand") || mraidHasCall(ctx, "resize") || mraidHasCall(ctx, "close") if changes && !mraidHasEventListener(ctx, "stateChange") { return mraidIssueFrom(rule, 0) } return nil }}, {ID: "output-script-hygiene", Severity: "requirement", Title: "Rejected script attributes found", Detail: "Playable output should not contain type=\"module\" or crossorigin on script tags; ad networks can reject ES modules/CORS attributes.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue { if mraidRejectedScriptAttrsRx.MatchString(ctx.HTML) { return mraidIssueFrom(rule, 0) } return nil }}, {ID: "console-error", Severity: "best-practice", Title: "console.error found", Detail: "The ship checklist expects no console.error calls in output; use guarded logging or remove debug error logs before submission.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue { if mraidConsoleErrorRx.MatchString(ctx.ScriptText) { return mraidIssueFrom(rule, 0) } return nil }}, {ID: "lifecycle-stubs", Severity: "requirement", Title: "Playable lifecycle signals not found", Detail: "Expose or call gameReady, gameStart, gameEnd, and gameClose so network preview tools can detect the playable lifecycle.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue { missing := []string{} for _, name := range []string{"gameReady", "gameStart", "gameEnd", "gameClose"} { if !mraidHasLifecycleSignal(ctx, name) { missing = append(missing, name) } } if len(missing) == 0 { return nil } found := mraidIssueFrom(rule, 0) found.Detail = found.Detail + " Missing: " + strings.Join(missing, ", ") + "." return found }}, }