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

` 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 }}, }