{"id":357268,"date":"2026-08-25T19:30:18","date_gmt":"2026-08-25T19:30:18","guid":{"rendered":"https:\/\/es.wordpress.org\/plugins\/walead\/"},"modified":"2026-08-25T22:17:10","modified_gmt":"2026-08-25T22:17:10","slug":"masvirtual-lead-tracking","status":"publish","type":"plugin","link":"https:\/\/as.wordpress.org\/plugins\/masvirtual-lead-tracking\/","author":15007376,"comment_status":"closed","ping_status":"closed","template":"","meta":{"version":"1.3.0","stable_tag":"1.3.0","tested":"7.1","requires":"6.4","requires_php":"8.0","requires_plugins":null,"header_name":"MasVirtual Lead Tracking","header_author":"M\u00e1sVirtual","header_description":"Lead attribution and a configurable WhatsApp button. Captures where every visitor came from, renders the contact link, and records each lead with its campaign.","assets_banners_color":"16283f","last_updated":"2026-08-25 22:17:10","external_support_url":"","external_repository_url":"","donate_link":"","header_plugin_uri":"https:\/\/masvirtual.co","header_author_uri":"","rating":0,"author_block_rating":0,"active_installs":0,"downloads":62,"num_ratings":0,"support_threads":0,"support_threads_resolved":0,"author_block_count":0,"sections":["description","installation","faq","changelog"],"tags":{"1.2.1":{"tag":"1.2.1","author":"jacobis1036","date":"2026-08-25 19:30:00"},"1.3.0":{"tag":"1.3.0","author":"jacobis1036","date":"2026-08-25 22:17:10"}},"upgrade_notice":{"1.3.0":"<p>When a customer who already wrote to you taps the button again, the bubble now says so.\nTheir record is left untouched: stage and outcome stay where they were.<\/p>","1.2.1":"<p>Security hardening. The board\/list view switch is now nonce-protected, and every database\nquery goes through prepare(), table names included, using the %i placeholder.<\/p>","1.2.0":"<p>Renamed to MasVirtual Lead Tracking. If you used them, <code>data-walead-*<\/code>, <code>window.walead<\/code>\nand <code>[walead_whatsapp]<\/code> are now <code>data-mvlt-*<\/code>, <code>window.mvlt<\/code> and <code>[mvlt_whatsapp]<\/code>. The\nclicks bubble can now be dismissed.<\/p>","1.1.1":"<p>Fixes a Canvas view whose header count disagreed with the board, and filter dropdowns\nthat could display a filter that was not actually applied.<\/p>","1.1.0":"<p>Fixes an export queue that accumulated conversions which could never be sent, and a\nCanvas view whose header count disagreed with the board. Also lowers the PHP\nrequirement to 8.0.<\/p>"},"ratings":[],"assets_icons":{"icon-256x256.png":{"filename":"icon-256x256.png","revision":3665914,"resolution":"256x256","location":"assets","locale":"","width":256,"height":256}},"assets_banners":{"banner-1544x500.png":{"filename":"banner-1544x500.png","revision":3665914,"resolution":"1544x500","location":"assets","locale":"","width":1544,"height":500},"banner-772x250.png":{"filename":"banner-772x250.png","revision":3665914,"resolution":"772x250","location":"assets","locale":"","width":772,"height":250}},"assets_blueprints":{},"all_blocks":[],"tagged_versions":["1.2.1","1.3.0"],"block_files":[],"assets_screenshots":{"screenshot-1.png":{"filename":"screenshot-1.png","revision":3665914,"resolution":"1","location":"assets","locale":"","width":2880,"height":3000},"screenshot-2.png":{"filename":"screenshot-2.png","revision":3665914,"resolution":"2","location":"assets","locale":"","width":2880,"height":3000},"screenshot-3.png":{"filename":"screenshot-3.png","revision":3665914,"resolution":"3","location":"assets","locale":"","width":2880,"height":1800},"screenshot-4.png":{"filename":"screenshot-4.png","revision":3665914,"resolution":"4","location":"assets","locale":"","width":2880,"height":3000},"screenshot-5.png":{"filename":"screenshot-5.png","revision":3665914,"resolution":"5","location":"assets","locale":"","width":2880,"height":3000},"screenshot-6.png":{"filename":"screenshot-6.png","revision":3665914,"resolution":"6","location":"assets","locale":"","width":2880,"height":1800},"screenshot-7.png":{"filename":"screenshot-7.png","revision":3665914,"resolution":"7","location":"assets","locale":"","width":1170,"height":2532}},"screenshots":{"1":"Dashboard: funnel from button view to closed sale, sources, campaigns and the pages\nthat generate clicks.","2":"Conversions: the conversion actions, the diagnostics that answer \"is this working?\",\nand every skipped row with its reason.","3":"Leads board: cards by funnel stage, with the reference each person carries in their\nmessage.","4":"Leads list, with filters by channel, stage, outcome, date range, sales rep and page.","5":"WhatsApp: recipients, routing, prefilled message and placeholders.","6":"The floating button on the site.","7":"The same button on mobile, where most of these conversations actually happen."}},"plugin_section":[],"plugin_tags":[9067,4196,1150,985,335],"plugin_category":[58],"plugin_contributors":[277476],"plugin_business_model":[],"class_list":["post-357268","plugin","type-plugin","status-publish","hentry","plugin_tags-attribution","plugin_tags-conversions","plugin_tags-crm","plugin_tags-google-ads","plugin_tags-leads","plugin_category-user-management","plugin_contributors-jacobis1036","plugin_committers-jacobis1036"],"banners":{"banner":"https:\/\/ps.w.org\/masvirtual-lead-tracking\/assets\/banner-772x250.png?rev=3665914","banner_2x":"https:\/\/ps.w.org\/masvirtual-lead-tracking\/assets\/banner-1544x500.png?rev=3665914","banner_rtl":false,"banner_2x_rtl":false},"icons":{"svg":false,"icon":"https:\/\/ps.w.org\/masvirtual-lead-tracking\/assets\/icon-256x256.png?rev=3665914","icon_2x":"https:\/\/ps.w.org\/masvirtual-lead-tracking\/assets\/icon-256x256.png?rev=3665914","generated":false},"screenshots":[{"src":"https:\/\/ps.w.org\/masvirtual-lead-tracking\/assets\/screenshot-1.png?rev=3665914","caption":"Dashboard: funnel from button view to closed sale, sources, campaigns and the pages\nthat generate clicks."},{"src":"https:\/\/ps.w.org\/masvirtual-lead-tracking\/assets\/screenshot-2.png?rev=3665914","caption":"Conversions: the conversion actions, the diagnostics that answer \"is this working?\",\nand every skipped row with its reason."},{"src":"https:\/\/ps.w.org\/masvirtual-lead-tracking\/assets\/screenshot-3.png?rev=3665914","caption":"Leads board: cards by funnel stage, with the reference each person carries in their\nmessage."},{"src":"https:\/\/ps.w.org\/masvirtual-lead-tracking\/assets\/screenshot-4.png?rev=3665914","caption":"Leads list, with filters by channel, stage, outcome, date range, sales rep and page."},{"src":"https:\/\/ps.w.org\/masvirtual-lead-tracking\/assets\/screenshot-5.png?rev=3665914","caption":"WhatsApp: recipients, routing, prefilled message and placeholders."},{"src":"https:\/\/ps.w.org\/masvirtual-lead-tracking\/assets\/screenshot-6.png?rev=3665914","caption":"The floating button on the site."},{"src":"https:\/\/ps.w.org\/masvirtual-lead-tracking\/assets\/screenshot-7.png?rev=3665914","caption":"The same button on mobile, where most of these conversations actually happen."}],"raw_content":"<!--section=description-->\n<p>Google Ads can optimise for what you report to it, and nothing more. If all you report\nis button clicks, that is what it buys you: clicks. The sale that closed three weeks\nlater, on WhatsApp, by a phone call your CRM never saw, is invisible to the bidding\nalgorithm.<\/p>\n\n<p>MasVirtual Lead Tracking closes that loop. It records where every visitor came from, keeps that origin\nattached to the lead through your sales funnel, and reports back to Google Ads at each\nstage \u2014 in the browser for the contact itself, and by offline conversion import for the\nqualified lead and the closed sale, which happen weeks later when nobody is on the page\nanymore.<\/p>\n\n<p>The WhatsApp button is part of it because that is where these conversations start, and\nbecause a click that is not measured cannot be attributed. But the button is the input,\nnot the point: the point is that \"campaign arl-transporte produced 3 sales worth\n9,600,000\" becomes a sentence you can read, and that Google Ads can bid on.<\/p>\n\n<p><strong>What this version does<\/strong><\/p>\n\n<ul>\n<li>Captures ad click identifiers (Google, Meta, Microsoft, TikTok), UTM parameters and\nthe referrer for every visitor, not just paid ones.<\/li>\n<li>Stores first touch and last touch separately, because they rarely match.<\/li>\n<li>Renders the floating button, with configurable icon, size, color, shape, position\nand business hours.<\/li>\n<li>Measures clicks on <strong>any<\/strong> WhatsApp link on the page, including the ones added by\nyour theme, an Elementor button or another plugin, and the ones that appear after\nthe page loads.<\/li>\n<li>Routes the click across several recipients, by rotation, by weight or by schedule,\nand can create the lead already assigned to a sales rep.<\/li>\n<li>Different messages per page, per content type or <strong>per traffic source<\/strong>: you can\ngreet someone who arrived from an ad differently.<\/li>\n<li><code>[mvlt_whatsapp]<\/code> shortcode and a Gutenberg block.<\/li>\n<li>Configurable retention with automatic purging, wired into the WordPress privacy\ntools.<\/li>\n<li>Reports conversions to Google Ads: in the browser with gtag for what happens on the\npage, and through CSV import for what happens weeks later, when a sales rep marks\nthe sale as won.<\/li>\n<li>A diagnostics screen that answers \"is this working?\" without opening the console,\nwith every warning explained and a button to simulate an event.<\/li>\n<\/ul>\n\n<p><strong>What it does not do yet<\/strong><\/p>\n\n<p>The drag-and-drop lead board with columns, and outgoing webhooks to n8n or a CRM, are\ncoming in the next release. Leads are already stored in full: nothing captured today\nis lost when those arrive.<\/p>\n\n<h3>About the reference in the message<\/h3>\n\n<p>MasVirtual Lead Tracking appends a short reference to the prefilled message, like <code>Ref: [#K7M2QF83]<\/code>.\nThat is what lets you match the message arriving in WhatsApp with the campaign that\nproduced it.<\/p>\n\n<p>Know this before you trust the number: between 15% and 30% of users delete the\nprefilled text before sending. Those leads cannot be matched. That is a WhatsApp\nlimitation, not a plugin one, and the only real mitigation is to push a form as the\nprimary channel when exact attribution matters.<\/p>\n\n<p>You can also place the reference mid-message using the <code>{ref}<\/code> placeholder, which\nsurvives partial deletion better than a trailing line does.<\/p>\n\n<h3>Privacy<\/h3>\n\n<p>Click identifiers are personal data under the GDPR in Europe, and under Law 1581 of\n2012 in Colombia. Storing them makes the site operator a data controller.<\/p>\n\n<p>Add a paragraph to your privacy policy along these lines:<\/p>\n\n<blockquote>\n  <p>When you visit this site we store, for up to 90 days, the advertising identifier\n  that came with your visit (for example the Google Ads one), the campaign parameters\n  in the URL, the address of the page you arrived from, and a random identifier for\n  your browser. We do this to know which campaigns generate real enquiries. If you\n  contact us on WhatsApp, we associate that data with your message. You may request\n  access to or deletion of this information by writing to [your contact address].<\/p>\n<\/blockquote>\n\n<p>The plugin reads the Google Consent Mode v2 state and acts accordingly: without\nconsent it writes no first-party cookie and keeps only the identifiers that do not\ndepend on cookies.<\/p>\n\n<p>What it stores in the browser, with consent, is this and nothing else:<\/p>\n\n<ul>\n<li><code>wl_v<\/code> \u2014 a first-party cookie holding a random browser identifier. The server\nwrites it, it is <code>HttpOnly<\/code> and <code>SameSite=Lax<\/code>, and it expires after the number of\ndays set as the attribution window. No script reads it, neither this plugin nor any\nother on the page.<\/li>\n<li><code>wl_f<\/code>, <code>wl_l<\/code>, <code>wl_uid<\/code>, <code>wl_turno<\/code>, <code>wl_vp<\/code> \u2014 in <code>localStorage<\/code>. These are not\ncookies and travel in no request: first touch, last touch, the lead reference, the\nassigned sales rep, and the browser-proposed visitor identifier used only until the\nserver issues the <code>wl_v<\/code> cookie. The proposed value is deliberately named\ndifferently from the cookie, because they are two separate things.<\/li>\n<\/ul>\n\n<p>There are no third-party cookies and no identifiers shared with anyone.<\/p>\n\n<p><strong>Conversion feed (optional, disabled by default).<\/strong> If you enable it, the plugin\npublishes a URL with a random 48-character key so that Google Ads can collect\nconversions on its own schedule. That URL serves a CSV containing Google click\nidentifiers, sale values and conversion action names, and it is readable by anyone who\nknows it: Google cannot authenticate against WordPress. It contains no names, phone\nnumbers or email addresses. The key can be rotated at any time, which invalidates the\nprevious URL immediately.<\/p>\n\n<h3>External services<\/h3>\n\n<p>The plugin works without contacting any server of ours: it sends no telemetry, phones\nnobody home, and requires no account or key. Everything it captures stays in the\nsite's own database.<\/p>\n\n<p>Two third-party services are involved, both by explicit decision of the administrator:<\/p>\n\n<p><strong>Google tag (gtag.js).<\/strong> Only if you tick \"let MasVirtual Lead Tracking load the Google tag\" under\nConversions, which ships <strong>disabled<\/strong>. It loads\n    https:\/\/www.googletagmanager.com\/gtag\/js and sends Google Ads the conversion event\nwith the identifier, label, value and currency you configured, plus the click\nidentifier Google placed in the URL. If your site already loads the tag through Site\nKit, GTM or a theme snippet, the plugin detects it and does not duplicate it. Google\nterms and privacy: https:\/\/policies.google.com\/terms \u00b7\nhttps:\/\/policies.google.com\/privacy<\/p>\n\n<p><strong>WhatsApp.<\/strong> The button and the links in your content point to <code>https:\/\/wa.me<\/code>, the\nofficial WhatsApp redirector. Nothing is sent to Meta from the server: it is the\nvisitor who navigates there on tap, with the number and message in the URL, exactly as\nwith any WhatsApp link. WhatsApp terms and privacy:\nhttps:\/\/www.whatsapp.com\/legal\/terms-of-service \u00b7\nhttps:\/\/www.whatsapp.com\/legal\/privacy-policy<\/p>\n\n<h3>About imported conversions<\/h3>\n\n<p>Conversions that happen after the visit \u2014 a qualified lead, a closed sale \u2014 cannot be\nreported from the browser, because the visitor is no longer there. They accumulate and\nare uploaded to Google Ads by file.<\/p>\n\n<p>Every row carries the event time with the site's UTC offset stated explicitly, and an\norder identifier derived from the lead reference. That identifier is what lets you\nupload the same file twice without duplicating conversions.<\/p>\n\n<p>Before writing each row the plugin validates that the conversion is later than the\nclick, that no more than 90 days have passed, and that the identifier is of a type\nGoogle accepts through this route. Rows that fail are shown on screen with the reason,\nnever dropped silently: a conversion that disappears without explanation is worse than\nan error, because the team believes it was reported.<\/p>\n\n<p>Rows whose problem can never be fixed \u2014 a lead that arrived through an unpaid channel,\na <code>gbraid<\/code>\/<code>wbraid<\/code> identifier, a conversion already past the 90-day window \u2014 are\ndiscarded with their reason recorded in the lead's history, so the \"pending\" counter\nonly ever shows work that still exists. Rows you can still fix, such as an action\nmissing its Google Ads name, stay pending on purpose.<\/p>\n\n<p>To automate the upload without a file, <code>GET \/wp-json\/mvlt\/v1\/pendientes<\/code> returns\nalready-validated conversions and <code>POST \/wp-json\/mvlt\/v1\/exportados<\/code> confirms the\nuploads. That is the recommended route with n8n: the Google Ads API PHP library is\nheavy and demands OAuth, and it is not worth it below roughly 200 conversions a month.<\/p>\n\n<!--section=installation-->\n<ol>\n<li>Upload the folder to <code>\/wp-content\/plugins\/<\/code> or install the ZIP from the admin.<\/li>\n<li>Activate it.<\/li>\n<li>Go to <strong>MasVirtual Lead Tracking \u2192 WhatsApp<\/strong> and add a phone number. That is enough to get going.<\/li>\n<\/ol>\n\n<p>If you serve the site behind full-page caching, purge the cache after activating: the\nstored HTML does not include the button yet.<\/p>\n\n<!--section=faq-->\n<dl>\n<dt id=\"i%20already%20have%20another%20whatsapp%20button%20plugin.%20do%20they%20clash%3F\"><h3>I already have another WhatsApp button plugin. Do they clash?<\/h3><\/dt>\n<dd><p>No, and this case is expected. MasVirtual Lead Tracking detects the other plugin's links and measures\nthem without changing them, which is its default behaviour. You can turn off MasVirtual Lead Tracking's\nown floating button and keep only the measurement.<\/p><\/dd>\n<dt id=\"does%20it%20work%20with%20full-page%20caching%3F\"><h3>Does it work with full-page caching?<\/h3><\/dt>\n<dd><p>Yes, that is a design requirement. Nothing that depends on the visitor is generated in\nPHP: attribution is resolved in the browser and travels over REST.<\/p><\/dd>\n<dt id=\"why%20does%20the%20contrast%20warning%20appear%20with%20the%20factory%20colors%3F\"><h3>Why does the contrast warning appear with the factory colors?<\/h3><\/dt>\n<dd><p>Because it is true: white on the WhatsApp green gives 2.0:1 and does not reach the\nrecommended 3:1. It is the color pair Meta uses in its own interface and the one people\nrecognise, so it ships as the default knowingly. If you prefer legibility over\nfamiliarity, darken the background.<\/p><\/dd>\n<dt id=\"i%20uninstalled%20the%20plugin.%20did%20i%20lose%20my%20leads%3F\"><h3>I uninstalled the plugin. Did I lose my leads?<\/h3><\/dt>\n<dd><p>No, unless you had ticked the deletion checkbox under Settings. Deactivating never\ndeletes anything.<\/p><\/dd>\n\n<\/dl>\n\n<!--section=changelog-->\n<h4>1.3.0<\/h4>\n\n<ul>\n<li><strong>New: the bubble now tells you when someone who already wrote to you comes back.<\/strong>\nThe lead reference lives in the visitor's <code>localStorage<\/code> and never expires, so when\nsomebody already in your funnel returned and tapped the button again, the click was\nfiled in their record and nothing else happened. No new lead, and the bubble only ever\nlooked at leads in the \"New\" stage created in the last few hours \u2014 so the advisor never\nfound out that one of their own customers was writing again. From an incognito window\nthe same person did show up, because the reference was gone and a second lead was born.\nThat asymmetry was the giveaway.<\/li>\n<li>Those returns get their own section in the panel, separate from the clicks awaiting\nconfirmation. <strong>Their record is not touched<\/strong>: the stage and the outcome stay exactly\nwhere they were. Sending a closed sale back to \"New\" would erase the work and could\nre-report a conversion that was already reported. It is a notice, not a question.<\/li>\n<li>The panel lost its explanatory paragraphs. Two headings \u2014 \"New\" and \"Came back\" \u2014 and\nthe rows. An advisor opens this twenty times a day and stops reading the help text after\nthe second, so all it did was push the buttons further down.<\/li>\n<li>Each return can be dismissed with \"Seen\", and comes back if that person taps again.\nThe acknowledgement is stored as one more entry in the lead's history, so the record\nreads \"came back\", \"seen by Carolina\" rather than just showing a final state.<\/li>\n<li><strong>The whole interface is now available in English.<\/strong> The plugin's source strings are\nwritten in Spanish, so a site running WordPress in English saw a Spanish admin. All 718\nstrings are translated and shipped as an <code>en_US<\/code> catalogue: the dashboard, the funnel,\nthe conversion screens, the WhatsApp settings, every warning and every error message.\nOther languages still go through translate.wordpress.org as usual.<\/li>\n<li>The acceptance test suites no longer depend on the site's language. Several of them\ncheck a warning by looking for a word inside its text, and the moment the English\ncatalogue arrived they started failing on an English site \u2014 the code was fine, the\nwarning appeared, it just said \"contrast\" where the test looked for \"contraste\". A\nsuite whose result depends on where it runs cannot tell you whether the code works.<\/li>\n<li><strong>Fixed: a method meant for exactly this was dead code.<\/strong> <code>abierto_por_visitante()<\/code>\nlooks for an open lead belonging to the same visitor within a window, and nothing ever\ncalled it. The second deduplication layer the code documents has never run: the only\none in effect is the browser reference.<\/li>\n<\/ul>\n\n<h4>1.2.1<\/h4>\n\n<ul>\n<li><strong>Fixed: switching between the board and the list view was a state-changing GET with no\nnonce.<\/strong> The <code>vista<\/code> parameter writes the chosen view to user meta, so loading a crafted\nURL while logged in flipped someone's preference behind their back. The damage was small \u2014\ntheir board moved \u2014 but a GET that writes without checking where the request came from is\nCSRF regardless, and the links are generated by the plugin, so signing them costs nothing.\nAn absent or expired nonce now falls back to the saved view instead of killing the page: a\nstale bookmark is not an attack.<\/li>\n<li><strong>Every query now goes through <code>$wpdb-&gt;prepare()<\/code>, including the table names.<\/strong> Fifty-two\ncall sites interpolated the table name into the SQL string, which was the only option\nbefore WordPress 6.2 introduced the <code>%i<\/code> identifier placeholder. This plugin requires 6.4,\nso there was no reason left to keep doing it. Where the <code>IN (\u2026)<\/code> list has a variable number\nof placeholders, those are still built with <code>array_fill()<\/code> \u2014 they are generated <code>%d<\/code> and\n  %s tokens, never data.<\/li>\n<li>Raw input that ends up in an admin notice is now sanitized where it is stored, not only\nescaped where it is printed. Those notices travel in a transient and are rendered on a\nlater request, so between the two the value is stored data. It is also truncated, because\nthe point is only to let the person recognise which of their lines failed.<\/li>\n<li>The date filters on the leads screen sanitize before validating. <code>fecha()<\/code> was already a\nstrict allow-list \u2014 exact <code>YYYY-MM-DD<\/code> plus <code>checkdate()<\/code> \u2014 but doing it in that order\nkeeps the intent visible on the line itself.<\/li>\n<li><strong>Fixed: the credit-in-footer test checked a screen that no longer existed.<\/strong> It hardcoded\nthe admin screen id, so the 1.2.0 rename left it asserting against a page nobody could\nreach. It now builds the id from <code>Menu::SLUG<\/code>.<\/li>\n<li>Corrected the plugin folder name in the testing instructions, which the rename had left\npointing at a directory that does not exist.<\/li>\n<\/ul>\n\n<h4>1.2.0<\/h4>\n\n<ul>\n<li><strong>Renamed.<\/strong> The plugin was called \"Walead\", a name already used by unrelated commercial\nlead-generation and WhatsApp marketing services with overlapping functionality. It is now\n\"MasVirtual Lead Tracking\", which carries the name of the people who build it and describes\nwhat it does. The rename goes all the way down: text domain, PHP namespace, option and\ntable prefixes, REST namespace, CSS classes and the <code>data-<\/code> attributes you can put on your\nown links. If you were using <code>data-walead-*<\/code> attributes or the <code>window.walead<\/code> object,\nthey are now <code>data-mvlt-*<\/code> and <code>window.mvlt<\/code>.<\/li>\n<li><strong>The unconfirmed-clicks bubble can now be dismissed.<\/strong> It shows across the whole admin on\npurpose \u2014 an advisor may be writing a post when a click arrives \u2014 but there was no way to\nput it away, which is not a reasonable thing to do to someone else's dashboard. It now has\na close button, the choice is remembered per user, and dismissing it stops the polling and\nthe stylesheet from loading outside this plugin's own screens. Inside them the bubble stays,\nand the same button brings it back everywhere. To switch it off entirely there is still the\nsetting in Settings.<\/li>\n<li>The bubble's polling interval went from 30 to 60 seconds. It runs on every admin screen, so\nat thirty seconds ten advisors with the dashboard open meant 1,200 requests an hour against\na shared host, and a click attended one minute later changes nothing.<\/li>\n<li><strong>Fixed: the dismissal endpoint would never have existed.<\/strong> Its route was registered from\nthe bubble's own class, which is only instantiated under <code>is_admin()<\/code> \u2014 and a request to\n  \/wp-json is not. It is now registered alongside the other REST routes, the one place that\nruns in both contexts.<\/li>\n<li>The admin menu label is shortened to \"Lead Tracking\" so it does not wrap onto two lines in\nthe sidebar. The page title keeps the full name.<\/li>\n<li>The plugin's own uninstall now removes the bubble's per-user preference unconditionally,\noutside the \"delete my data\" checkbox. That checkbox exists to protect commercial history,\nand an interface flag is not that \u2014 leaving it behind would orphan one <code>usermeta<\/code> row per\nadvisor.<\/li>\n<\/ul>\n\n<h4>1.1.1<\/h4>\n\n<ul>\n<li><strong>Fixed: in the Canvas view the header count and the board disagreed.<\/strong> Filtering by\noutcome said \"6 leads\" while the board still drew all 28. The outcome filter now\nreaches the cards: wins and losses are spread across several stages, so filtering by\noutcome trims the board instead of emptying it, and \"open opportunities only\" is the\nquery a coordinator runs most. The stage dropdown, which genuinely does not belong in\na board whose columns <em>are<\/em> the stages, is no longer rendered in that view, and the\nheader stopped applying it. A control that promises and does not deliver is worse than\nan absent one, because it teaches you to distrust the rest.<\/li>\n<li><strong>Fixed: the filter dropdowns could show a filter that was not applied.<\/strong> Browsers\nrestore <code>select<\/code> values on reload and on back-navigation, so the screen could read\n\"Lost\" while the query filtered nothing \u2014 and the only hint was the absence of the\n\"Clear filters\" link, which nobody looks at. Reported twice as \"the filter is broken\",\nand both times it was this. The filter form now declares <code>autocomplete=\"off\"<\/code>, so what\nthe controls show is always what was applied.<\/li>\n<\/ul>\n\n<h4>1.1.0<\/h4>\n\n<ul>\n<li><strong>Fixed: the export queue accumulated conversions that could never be sent.<\/strong> A lead\nthat arrived through organic search, direct traffic or Meta never gets a <code>gclid<\/code>, but\na row was queued for it anyway and re-evaluated on every single export, forever. The\n\"pending export\" counter kept a number that matched no real work \u2014 the worst kind of\ncounter, the one that teaches you to ignore it. Rows whose problem is irreversible\nare now discarded with their reason, and each one leaves a record in its lead's\nhistory. Rows that a human can still fix \u2014 a missing Google Ads action name, a\nconversion time entered wrong \u2014 stay pending on purpose, because discarding those\nwould take the fix out of the hands of the person who can make it.<\/li>\n<li>The <code>localStorage<\/code> key holding the browser-proposed visitor identifier is now\n  wl_vp instead of sharing the <code>wl_v<\/code> name with the first-party cookie. They were\nalways two different things \u2014 the cookie is the identity the server decides, and this\nis only a proposal \u2014 but the shared name made the privacy section look like it\ncontradicted what an auditor sees in the inspector.<\/li>\n<li>Minimum PHP lowered to 8.0. Nothing in the codebase used 8.1 syntax; the requirement\nwas conservative and blocked installation on hosts that are still on 8.0.<\/li>\n<li>The readme is now in English, as the plugin directory requires. Spanish and other\ntranslations go through translate.wordpress.org.<\/li>\n<li><strong>Fixed: a PHP notice on every admin screen under WordPress 6.7 and later.<\/strong> The\nunconfirmed-clicks notice decided whether to register itself during <code>plugins_loaded<\/code>,\nand that question read a plugin option \u2014 which pulls in the defaults, which carry\ntranslated strings. Asking for a translation before <code>init<\/code> makes WordPress load the\ntext domain too early and complain about it. The same call also ran\n  current_user_can() before authentication plugins had finished registering, which is\nan answer worth not trusting. Both checks now happen inside the hooks that render the\nbubble, which run well after <code>init<\/code>.<\/li>\n<li>Removed <code>load_plugin_textdomain()<\/code>: WordPress has loaded translations for directory\nplugins by itself since 4.6, and calling it before <code>init<\/code> emits a notice since 6.7.<\/li>\n<\/ul>\n\n<h4>1.0.9<\/h4>\n\n<ul>\n<li><strong>Fixed: the page filter did nothing in the Canvas view.<\/strong> It was wired to the PHP\nlisting but not to the canvas, which loads its cards over REST and never sent the\nparameter. The symptom was misleading: the header count did change \u2014 PHP computes\nthat \u2014 and the cards did not.<\/li>\n<li><strong>The landing page, in the lead's attribution.<\/strong> The card now tells the whole story:\n\"arrived on \/afiliacion-transporte\/ with campaign arl-transporte and tapped the\nbutton on the home page\". Those are two different questions and until now only one\ncould be answered.<\/li>\n<li>URLs are shown stripped of gclid and campaign parameters, with the original in the\ntitle and a link to the real page. Normalised with the same function that groups the\nreport, so what a card says matches the dashboard table.<\/li>\n<\/ul>\n\n<h4>1.0.8<\/h4>\n\n<ul>\n<li><strong>Which page each person tapped the button on<\/strong>, which is not the same as where they\ncame in. Someone can land on a campaign page, not be convinced, browse to the home\npage and write from there: looking only at the channel, that landing page looks like\nit works.<\/li>\n<li><strong>\"Pages that generate clicks\"<\/strong> table on the dashboard, with clicks, closed deals,\nclose rate and value. All four columns together because \"clicks\" alone misleads: a\npage with high volume and zero closes is exactly the one to fix.<\/li>\n<li><strong>Page filter<\/strong> on the Leads screen, populated automatically with the pages that have\nreceived clicks in the last 90 days.<\/li>\n<li><strong>The click page in the lead panel<\/strong>, visible and timestamped, because it is context a\nsales rep uses when replying.<\/li>\n<li>Works <strong>retroactively<\/strong>: the data has travelled in the event log since the first\nversion, so the table fills with everything captured to date without any migration.<\/li>\n<li>URLs are normalised before grouping. Without that, every Google Ads click arrives with\nits own gclid and twenty clicks on the same page would look like twenty pages.<\/li>\n<\/ul>\n\n<h4>1.0.7<\/h4>\n\n<ul>\n<li><strong>Date range and sales rep filters<\/strong> on the Leads screen, in both views. Previously\nyou could only filter by channel, stage, outcome and search.<\/li>\n<li>Dates are interpreted as whole days in the site's timezone, not UTC: in Colombia\n\"today\" starts at 05:00 UTC, and without the conversion filtering by today left out\nthe small-hours leads and included last night's.<\/li>\n<li>\"Unassigned\" is an option in the sales rep filter, usually a coordinator's most\nuseful query.<\/li>\n<li>A sales rep cannot request another's leads by changing the parameter: their own filter\nis applied after reading it.<\/li>\n<li>A link to clear the filters, visible only when some are set.<\/li>\n<\/ul>\n\n<h4>1.0.6<\/h4>\n\n<ul>\n<li><strong>Unconfirmed clicks notice in the admin bar.<\/strong> A counter of clicks awaiting a reply,\nand a panel where each is resolved with \"Yes, they wrote\" or \"No, they didn't\". This\nfixes the weakest link in the module: nothing can detect that someone wrote on\nWhatsApp \u2014 a person records that \u2014 and if they don't, the \"Wrote\" stage stays empty\nand there are no conversions to report to Google Ads.<\/li>\n<li>It appears throughout the WordPress admin, not only on the plugin's screens: a sales\nrep may be anywhere when a click arrives.<\/li>\n<li>\"Yes\" moves the stage to Wrote and leaves the opportunity open. \"No\" marks the outcome\nas discarded without touching the stage, because that click was never a conversation.\nBoth are reversible from the lead card.<\/li>\n<li>It only asks about clicks from the last 48 hours, configurable. Without a limit it\nwould ask for confirmation of clicks from weeks ago that nobody remembers.<\/li>\n<li>A sales rep only confirms their own. It can be switched off under Settings.<\/li>\n<li>It is a <strong>floating bubble<\/strong> with its own number, not a screen notice: it grows on its\nown every 30 seconds without reloading, gives a small jump when it rises, and the\nnumber also appears in the tab title \u2014 which is the only thing visible when the rep is\non another tab. At zero it stays visible but dimmed, in grey.<\/li>\n<li>Each row shows the <strong>exact time of the click<\/strong> next to the relative one, in the site's\ntimezone, so the rep can compare it against their chat clock. With the day included\nwhen it is not today.<\/li>\n<li>Newest always on top. If a click arrives with the panel open it is not inserted\nautomatically: a \"1 new click arrived \u2014 show\" pill appears instead. Inserting it would\npush the list just as someone moves the cursor to a button, and in a panel where every\nclick writes to the database that is not a detail.<\/li>\n<li>The reference is also visible on the board card, monospaced and in amber: it is the\ncode the person carries in their message, and the rep compares it at a glance.<\/li>\n<\/ul>\n\n<h4>1.0.5<\/h4>\n\n<ul>\n<li><strong>No REST route of the plugin can be cached.<\/strong> 1.0.3 protected only the feed and was\ntoo narrow a fix: the problem was the whole namespace. LiteSpeed caches all of\n  mvlt\/v1, authenticated requests included, and that produced three failures that\nlook nothing alike:<\/li>\n<li>Moving a lead on the board saved correctly but reverted to its previous stage on\nrefresh \u2014 the <code>PATCH<\/code> wrote and the following <code>GET<\/code> read a frozen list. It looked like\nsaving was failing, and it wasn't.<\/li>\n<li><code>\/turno<\/code>, which distributes sales reps by strict rotation, handed the same rep to\neverybody. That is the worst of the three because nobody would ever have noticed.<\/li>\n<li>And the feed one, already known.<\/li>\n<li><strong>The lead reference is now visible on the canvas card<\/strong>, monospaced with an amber\nbackground, as the first item in the header. It is the code the person carries in their\nWhatsApp message: the rep reads it in the chat and finds the matching card without\nopening anything. It is also in the <code>aria-label<\/code>, for screen reader users.<\/li>\n<\/ul>\n\n<h4>1.0.4<\/h4>\n\n<ul>\n<li><strong>You can now delete a lead from the interface<\/strong>, in the listing, with confirmation and\nonly for users who can configure the plugin: a sales rep corrects a lead, they don't\nmake it disappear.<\/li>\n<li><strong>A button to delete all the leads created by \"Simulate an event\"<\/strong> at once, which used\nto pile up and pollute the reports. It appears only if there are any and it goes by the\nfake gclid, so it can never reach a real lead.<\/li>\n<li><strong>Fixed: deleting a lead left its conversions orphaned in the queue.<\/strong> The exporter\nreported them on every attempt as \"the lead no longer exists\", dragging a problem with\nno solution forward forever. They are now discarded with their reason recorded.<\/li>\n<\/ul>\n\n<h4>1.0.3<\/h4>\n\n<ul>\n<li><strong>Fixed: a page cache could freeze the feed.<\/strong> It was assumed a REST route is never\ncached. LiteSpeed does cache them \u2014 it has a \"Cache REST API\" option that is often on \u2014\nand the effect was the worst possible: Google requested the URL every day and always\nreceived the same file, usually the empty one from the first request, with the schedule\nshowing green and zero conversions imported. Found in production, not in testing.<\/li>\n<li>The feed now declares <code>DONOTCACHEPAGE<\/code>, fires <code>litespeed_control_set_nocache<\/code> and sends\n  X-LiteSpeed-Cache-Control: no-cache, which covers LiteSpeed, WP Rocket, W3 Total\nCache, WP Super Cache and Comet Cache.<\/li>\n<li>Enabling or rotating the key purges the URL, because an already-stored entry is served\nby the web server without executing PHP and no signal arrives in time.<\/li>\n<li>An on-screen notice explains how to verify it, because no plugin can guarantee the\nbehaviour of the host's cache.<\/li>\n<\/ul>\n\n<h4>1.0.2<\/h4>\n\n<ul>\n<li>The MV Gclid Store importer now also runs from <strong>MasVirtual Lead Tracking \u2192 Settings<\/strong>, with the full\npreview visible above the button. Most shared hosts give no WP-CLI access, and a\nmigration that can only be run from a terminal is a migration that does not get run.<\/li>\n<li>The preview is the real run in dry mode \u2014 the same path and the same decisions, without\nwriting \u2014 and not a separate simulation that might not match what happens when you\npress the button.<\/li>\n<li>The block only appears if something is left to import, and disappears when it isn't.<\/li>\n<\/ul>\n\n<h4>1.0.1<\/h4>\n\n<ul>\n<li><strong>Importer from MV Gclid Store.<\/strong> <code>wp mvlt importar-mv<\/code> brings the leads from the\nprevious setup preserving their state. The single <code>estado<\/code> field, which mixed funnel\nposition and outcome, is split into MasVirtual Lead Tracking's stage\/outcome pair. It also brings the\ngclid as first touch and respects the original click time, without which imported leads\nwould be neither exportable nor useful for any report by date. It has <code>--en-seco<\/code>, it is\nidempotent, and it deliberately does not queue conversions: the ones already reported\nfrom the previous setup would be duplicated under a different Order ID.<\/li>\n<\/ul>\n\n<h4>1.0.0<\/h4>\n\n<ul>\n<li><strong>Scheduled feed for Google Ads.<\/strong> A keyed URL that Google fetches on its own schedule,\nfrom Goals \u2192 Conversions \u2192 Uploads \u2192 Schedules, with HTTPS as the source. It supports\nthe optional username and password Google offers. Disabled by default; the key is\nrotated or switched off in one click.<\/li>\n<li><strong>File format corrected against Google's specification.<\/strong> First row\n  Parameters:TimeZone, headers without quotes, and the UTC offset in four digits. The\ntime was also not being written in the right zone: it was formatted in UTC with the\nlocal offset appended, which shifted every conversion by five hours in Colombia and\nmoved small-hours ones to the wrong day.<\/li>\n<li><strong>Consent columns<\/strong> <code>Ad User Data<\/code> and <code>Ad Personalization<\/code>, required by the EU user\nconsent policy.<\/li>\n<li><strong>The visitor cookie is written by the server<\/strong>, not by <code>document.cookie<\/code>. A cookie set\nover HTTP does not fall under the seven-day cap Safari applies to anything a script\nwrites, so the attribution window is respected on iOS. And the server recognises a\nreturning visitor even if the browser lost <code>localStorage<\/code>, which previously counted them\nas a new visitor on every load.<\/li>\n<li><strong>Exported conversions leave a record on the lead card.<\/strong> Previously the history said\n\"queued for export\" forever, even months after Google had received it.<\/li>\n<\/ul>\n\n<h4>0.1.0<\/h4>\n\n<ul>\n<li>First release: attribution capture, the complete WhatsApp module, Google Ads conversions\nwith diagnostics and export, tables, roles, privacy and purging.<\/li>\n<\/ul>","raw_excerpt":"Connects WhatsApp conversations back to the Google Ads campaign that produced them, and reports qualified leads and closed sales to Ads.","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/as.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin\/357268","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/as.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin"}],"about":[{"href":"https:\/\/as.wordpress.org\/plugins\/wp-json\/wp\/v2\/types\/plugin"}],"replies":[{"embeddable":true,"href":"https:\/\/as.wordpress.org\/plugins\/wp-json\/wp\/v2\/comments?post=357268"}],"author":[{"embeddable":true,"href":"https:\/\/as.wordpress.org\/plugins\/wp-json\/wporg\/v1\/users\/jacobis1036"}],"wp:attachment":[{"href":"https:\/\/as.wordpress.org\/plugins\/wp-json\/wp\/v2\/media?parent=357268"}],"wp:term":[{"taxonomy":"plugin_section","embeddable":true,"href":"https:\/\/as.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_section?post=357268"},{"taxonomy":"plugin_tags","embeddable":true,"href":"https:\/\/as.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_tags?post=357268"},{"taxonomy":"plugin_category","embeddable":true,"href":"https:\/\/as.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_category?post=357268"},{"taxonomy":"plugin_contributors","embeddable":true,"href":"https:\/\/as.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_contributors?post=357268"},{"taxonomy":"plugin_business_model","embeddable":true,"href":"https:\/\/as.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_business_model?post=357268"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}