Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
a6f20cc
Feat: Views & Conversion Rate columns in the Forms table (#2973)
vanshk141999 Aug 20, 2026
a7a2b70
fix: hide the Views and Conversion Rate columns when tracking is off
vanshk141999 Aug 20, 2026
4173b16
fix: actually hide the columns — the flag crossed the PHP/JS boundary…
vanshk141999 Aug 20, 2026
de06e24
feat: measure conversion from when view tracking started
vanshk141999 Aug 21, 2026
a77c380
fix: keep counting views while the columns are hidden
vanshk141999 Aug 21, 2026
f56aaa6
test: cover get_tracking_started_at()
vanshk141999 Aug 21, 2026
6940a88
i18n: drop the version placeholder from the tracking setting description
vanshk141999 Aug 21, 2026
9cdf518
fix: stop the Edit Form pill overlapping fields, and attribute its cl…
vanshk141999 Aug 24, 2026
d4febaf
test: pin the Edit Form pill's flow-layout CSS contract
vanshk141999 Aug 24, 2026
05b4265
feat: report form-views usage to analytics
vanshk141999 Aug 24, 2026
45b3f0b
feat: make view tracking opt-in, stamped once on first enable
vanshk141999 Aug 24, 2026
dbce217
test: cover maybe_start_tracking() directly
vanshk141999 Aug 24, 2026
d517a05
fix: address the review findings on the views and conversion feature
vanshk141999 Aug 24, 2026
98b42f9
Merge pull request #3081 from brainstormforce/master-dev-2.12.5-psot
vanshk141999 Aug 26, 2026
616604c
Merge pull request #3082 from brainstormforce/dev-nr-2.12.5-post
vanshk141999 Aug 26, 2026
1a7db3e
Merge branch 'next-release' into feat/2973-forms-table-views-conversi…
vanshk141999 Aug 27, 2026
5ba9525
fix: address review findings on the form-views feature
vanshk141999 Aug 27, 2026
2ea1536
test: cover maybe_repair_tracking_window() across all three branches
vanshk141999 Aug 27, 2026
b943c15
Merge branch 'dev' into fix/edit-form-pill-overlap-analytics
vanshk141999 Aug 27, 2026
44534f1
fix: address review findings on the edit-form pill and its analytics
vanshk141999 Aug 27, 2026
0bb418b
test: cover add_removable_query_args() and its filter wiring
vanshk141999 Aug 27, 2026
53a0a65
fix: show only one SureForms admin notice at a time, and unify their …
vanshk141999 Aug 27, 2026
f51f0c8
test: cover the extracted prompt predicate and shared notice styles
vanshk141999 Aug 27, 2026
a8bd124
Merge notice collision + shared styling work into the edit-pill PR
vanshk141999 Aug 27, 2026
e09c877
feat: detect a missing entries table and offer a one-click repair
vanshk141999 Aug 27, 2026
c6c787f
test: name the new tests for the coverage gate and cover the remainin…
vanshk141999 Aug 27, 2026
f4510d7
fix: confirm the repair on the React path instead of silently droppin…
vanshk141999 Aug 27, 2026
222f2f8
feat: add an Enable Logs toggle that captures submission failures to …
vanshk141999 Aug 27, 2026
f7bebdd
test: cover the log route and the download/clear handlers
vanshk141999 Aug 27, 2026
8f736ad
feat: log by default and drop the seven-day expiry
vanshk141999 Aug 27, 2026
da626ca
feat: log every form submission failure, not just the ones that reach…
vanshk141999 Aug 31, 2026
8486607
feat: log what /submit-form actually said, not just that it responded
vanshk141999 Aug 31, 2026
33d5f08
feat: warn the site owner when submissions keep failing
vanshk141999 Aug 31, 2026
23ed978
fix: hide the log actions when there is nothing logged, and stop unde…
vanshk141999 Aug 31, 2026
48a5926
fix: keep Download and Clear visible, drop only the Empty label
vanshk141999 Aug 31, 2026
411fb85
Fix payment smart tag IDOR: drop unverified payment fields (#3090)
vanshk141999 Aug 31, 2026
ca3b830
Defer plugin_activated referer read to shutdown to avoid a race
adi3890 Aug 31, 2026
d48c801
Fix CI failures: method ordering + add test coverage
adi3890 Aug 31, 2026
0ea0fb4
Merge pull request #3093 from brainstormforce/fix/plugin-activated-re…
vanshk141999 Aug 31, 2026
d8e82a8
Merge pull request #3091 from brainstormforce/fix/payment-smart-tag-idor
vanshk141999 Sep 1, 2026
ebfe774
fix: address review on missing-entries-table repair (adopt safety, ca…
vanshk141999 Sep 1, 2026
f26a78a
fix: satisfy CI on the repair review changes
vanshk141999 Sep 1, 2026
3980aa8
fix: address review — stop double-escaping notice text, use raw edit-…
vanshk141999 Sep 1, 2026
f2ac107
Merge pull request #3062 from brainstormforce/fix/edit-form-pill-over…
vanshk141999 Sep 1, 2026
f9b5592
Merge branch 'dev' into fix/detect-missing-entries-table
vanshk141999 Sep 1, 2026
339ab66
fix: address review on Views & Conversion Rate columns
vanshk141999 Sep 1, 2026
71ca119
Merge pull request #3085 from brainstormforce/fix/detect-missing-entr…
vanshk141999 Sep 1, 2026
1efc9d5
Merge pull request #3061 from brainstormforce/feat/2973-forms-table-v…
vanshk141999 Sep 1, 2026
4189c85
feat: Form Checks panel, caching-plugin detection, and notice analytics
vanshk141999 Sep 1, 2026
3916f7f
Merge origin/dev into feat/client-error-logging
vanshk141999 Sep 1, 2026
e6d0604
fix: address review findings on the client log and action items
vanshk141999 Sep 1, 2026
d6a7f06
test: cover the four functions the coverage gate flagged
vanshk141999 Sep 1, 2026
843bbb3
fix: default srfm_enable_logs to on in every path, not just the runtime
vanshk141999 Sep 1, 2026
2cdf805
Merge pull request #3086 from brainstormforce/feat/client-error-logging
vanshk141999 Sep 1, 2026
8400844
Merge dev into dev-nr-2.12.6
vanshk141999 Sep 1, 2026
3d800cd
Merge pull request #3095 from brainstormforce/dev-nr-2.12.6
vanshk141999 Sep 1, 2026
27acd94
Version Bump 2.12.6
vanshk141999 Sep 1, 2026
dc5de7f
Update 2.12.6 changelog
vanshk141999 Sep 1, 2026
9823ef7
Merge pull request #3096 from brainstormforce/version-bump-2.12.6
vanshk141999 Sep 1, 2026
f21e688
chore: update i18n translations
github-actions[bot] Sep 1, 2026
9d8d2cb
Merge pull request #3098 from brainstormforce/i18n/next-release
vanshk141999 Sep 1, 2026
1bf73c7
fix: address the pre-merge findings on the 2.12.6 release PR
vanshk141999 Sep 1, 2026
0408521
Merge pull request #3099 from brainstormforce/fix/3097-review-findings
vanshk141999 Sep 1, 2026
1da8f3f
feat: retire the failure notice once reported, and set the 2.12.6 dat…
vanshk141999 Sep 2, 2026
980ad7d
test: cover get_last_fault_time()
vanshk141999 Sep 2, 2026
ef988d4
Merge pull request #3100 from brainstormforce/fix/2.12.6-release-date…
vanshk141999 Sep 2, 2026
a906f72
feat: separate notices for submission, notification and integration f…
vanshk141999 Sep 2, 2026
6357d23
test: cover the three functions the coverage gate flagged
vanshk141999 Sep 2, 2026
3453f5f
Merge pull request #3101 from brainstormforce/feat/detailed-failure-l…
vanshk141999 Sep 2, 2026
87c1d94
Merge pull request #3097 from brainstormforce/next-release
vanshk141999 Sep 2, 2026
c8df1c8
chore: strip internal-only paths from public mirror sync
vanshk141999 Sep 2, 2026
43f00e6
Sync master from upstream
vanshk141999 Sep 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 7 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
**Requires at least:** 6.4
**Tested up to:** 7.1
**Requires PHP:** 7.4
**Stable tag:** 2.12.5
**Stable tag:** 2.12.6
**License:** GPLv2 or later
**License URI:** http://www.gnu.org/licenses/gpl-2.0.html

Expand Down Expand Up @@ -426,22 +426,17 @@ Visit the [SureForms features page](https://sureforms.com/features/?utm_source=w
You can report security issues through our [Bug Bounty Program](https://brainstormforce.com/bug-bounty-program/). We collaborate with Patchstack to validate, triage, and handle security reports.

## Changelog ##
### 2.12.6 - 2nd September 2026 ###
* New: Added an Enable Logs option that records form submission failures to a downloadable file, making it easier to diagnose issues on a live site.
* New: Added Views and Conversion Rate columns to the Forms list, so you can see how each form is performing at a glance.
* New: SureForms now detects a missing entries database table and offers a one-click repair, so submissions start saving again without manual database work.
* Fix: The Edit Form button no longer overlaps form fields on the front end.
* Fix: This update addressed a security bug. Props to Patchstack for reporting it responsibly to our team.
### 2.12.5 - 25th August 2026 ###
* Fix: Form submissions now go through reliably, even on sites where a performance plugin combines or defers JavaScript.
### 2.12.4 - 19th August 2026 ###
* Improvement: Email notification images are now saved to the Media Library instead of being embedded as base64, improving email deliverability and reducing message size.
* Fix: Compatibility with WordPress 7.1.
### 2.12.3 - 6th August 2026 ###
* New: Added an "Entries" shortcut in the Instant Form admin bar to open a form's entries in one click.
* Improvement: Enhanced WPML compatibility. [https://wpml.org/plugin/sureforms/](https://wpml.org/plugin/sureforms/)
* Fix: This update addressed a security bug. Props to daroo and Wordfence for reporting it responsibly to our team.
* Fix: Cloudflare Turnstile no longer shows a warning on WordPress 7.0.
* Fix: Enhanced entries search - includes submitted data too.
* Fix: Fixed a failed database update that could stop forms from accepting submissions.
* Fix: Forms with default styles disabled no longer break the page — previously this aborted setup of every later form on the same page.
* Fix: Payment History styles load only on required pages.
* Fix: Removed the entries "Language" column. Sorting or filtering entries by language is no longer available, including via the entries API.
* Fix: The number field no longer looks misaligned when "Use label as placeholder" is on.
The full changelog is available [here](https://sureforms.com/whats-new/?utm_source=wordpress.org&utm_medium=whats_new).

## Upgrade Notice ##
1,303 changes: 1,178 additions & 125 deletions admin/admin.php

Large diffs are not rendered by default.

125 changes: 119 additions & 6 deletions admin/analytics.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@

namespace SRFM\Admin;

use SRFM\Inc\Database\Register;
use SRFM\Inc\Database\Tables\Entries;
use SRFM\Inc\Form_Views;
use SRFM\Inc\Helper;
use SRFM\Inc\Learn;
use SRFM\Inc\Traits\Get_Instance;
Expand Down Expand Up @@ -129,27 +131,38 @@ public static function events() {
* @return array
*/
public function add_srfm_analytics_data( $stats_data ) {
$stats_data['plugin_data']['sureforms'] = [
$stats_data['plugin_data']['sureforms'] = [
'free_version' => SRFM_VER,
'site_language' => get_locale(),
'most_used_anti_spam' => $this->most_used_anti_spam(),
'user_status' => $this->user_status(),
'pointer_popup_clicked' => $this->pointer_popup_clicked(),
];
// Every query against the entries table errors when the table is missing, so
// resolve that once here rather than letting each caller below trip over it.
$entries_table_missing = Register::is_entries_table_missing();

$stats_data['plugin_data']['sureforms']['numeric_values'] = [
'total_forms' => wp_count_posts( SRFM_FORMS_POST_TYPE )->publish ?? 0,
'instant_forms_enabled' => $this->instant_forms_enabled(),
'forms_using_custom_css' => $this->forms_using_custom_css(),
'ai_generated_forms' => $this->ai_generated_forms(),
'ai_generated_payment_forms' => $this->ai_generated_forms( 'payments' ),
'payment_forms' => $this->get_payment_forms_count(),
'total_entries' => Entries::get_total_entries_by_status(),
'total_entries' => $entries_table_missing ? 0 : Entries::get_total_entries_by_status(),
'restricted_forms' => $this->get_restricted_forms(),
'embed_styling_gb_default' => self::embed_styling_gutenberg_count( 'default' ),
'embed_styling_el_default' => self::embed_styling_elementor_count( 'default' ),
'embed_styling_br_default' => self::embed_styling_bricks_count( 'default' ),
'total_form_views' => $this->total_form_views(),
'forms_with_views' => $this->forms_with_views(),
];

// Whether the entries table is currently missing. An event fires once when a
// site first sees the notice; this is what shows the state persisting, and
// catches a recurrence that the event's one-time dedup would swallow.
$stats_data['plugin_data']['sureforms']['boolean_values']['db_entries_table_missing'] = $entries_table_missing;

$stats_data['plugin_data']['sureforms'] = array_merge_recursive( $stats_data['plugin_data']['sureforms'], $this->global_settings_data() );
// Add KPI tracking data.
$kpi_data = $this->get_kpi_tracking_data();
Expand Down Expand Up @@ -488,9 +501,78 @@ public function global_settings_data() {
// Payment analytics - check if any payment method is enabled.
$global_data['boolean_values']['stripe_enabled'] = $this->is_stripe_enabled();

// Delegated rather than read from $general_settings directly, so the telemetry
// can never disagree with what the Forms list actually shows. That matters
// because the default has already changed once: reimplementing the check here
// means a future change has two places to be made and one to be forgotten.
$global_data['boolean_values']['form_views_columns_enabled'] = Form_Views::get_instance()->is_tracking_enabled();

return $global_data;
}

/**
* Total page views counted across all published forms.
*
* Paired with `forms_with_views` and the existing `total_entries`, this is what
* makes the conversion figure interpretable in the warehouse: a site with views
* but no entries reads very differently from one with neither.
*
* Counts only published forms, so views left behind by a trashed or draft form
* do not inflate the total against a denominator that no longer includes them.
*
* @since 2.12.6
* @return int
*/
public function total_form_views() {
global $wpdb;

// Deliberately uncached: the only caller is the analytics payload builder,
// which runs once per cron pass. A cache would never see a second read inside
// a request, and any TTL would always have expired between passes — while
// still going stale if a second caller ever appeared.
// PHPCS: Ignore direct database query warning, as there is no built-in alternative.
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$total = $wpdb->get_var(
$wpdb->prepare(
"
SELECT SUM( pm.meta_value )
FROM {$wpdb->postmeta} pm
INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
WHERE pm.meta_key = %s
AND p.post_type = %s
AND p.post_status = 'publish'
",
Form_Views::META_KEY,
SRFM_FORMS_POST_TYPE
)
);

return Helper::get_integer_value( $total );
}

/**
* Number of published forms that have been viewed at least once.
*
* Distinguishes "the feature is on but nothing is embedded anywhere" from "the
* beacon is firing" — a total alone cannot, because one busy form looks the
* same as many quiet ones.
*
* @since 2.12.6
* @return int
*/
public function forms_with_views() {
return $this->custom_wp_query_total_posts(
[
[
'key' => Form_Views::META_KEY,
'value' => 0,
'compare' => '>',
'type' => 'NUMERIC',
],
]
);
}

/**
* Returns user status.
*
Expand Down Expand Up @@ -719,6 +801,22 @@ public function track_embed_styling_configured( $post_id, $post ) {
);
}

/**
* Track the plugin_activated event with the correct install referer.
*
* Dedup in self::events()->track() ensures this fires only once. Runs on
* 'shutdown' (see detect_state_events()) so it reads bsf_product_referers
* after any late-writing referer call has had a chance to run.
*
* @since 2.12.6
* @return void
*/
public function track_plugin_activated_event() {
$bsf_referrers = get_option( 'bsf_product_referers', [] );
$source = ! empty( $bsf_referrers['sureforms'] ) ? $bsf_referrers['sureforms'] : 'self';
self::events()->track( 'plugin_activated', SRFM_VER, [ 'source' => $source ] );
}

/**
* Extract non-inherit formTheme values from Bricks element data.
*
Expand Down Expand Up @@ -824,6 +922,14 @@ private function get_kpi_tracking_data() {
* @return int Daily submissions count.
*/
private function get_daily_submissions_count( $date ) {
// Guarded here rather than at the call site: this is the function that runs
// the query, so a future caller is covered too. Every read against a missing
// entries table raises a DB error, and the daily send would raise one per
// day counted on exactly the sites whose breakage we most need reported.
if ( Register::is_entries_table_missing() ) {
return 0;
}

$start_date = $date . ' 00:00:00';
$end_date = $date . ' 23:59:59';

Expand Down Expand Up @@ -853,10 +959,17 @@ private function get_daily_submissions_count( $date ) {
* @return void
*/
private function detect_state_events() {
// plugin_activated: dedup in self::events()->track() ensures this fires only once.
$bsf_referrers = get_option( 'bsf_product_referers', [] );
$source = ! empty( $bsf_referrers['sureforms'] ) ? $bsf_referrers['sureforms'] : 'self';
self::events()->track( 'plugin_activated', SRFM_VER, [ 'source' => $source ] );
// plugin_activated: deferred to 'shutdown' so that a referring plugin/theme's
// own BSF_UTM_Analytics::update_referer() call — which some products (incorrectly)
// make only after their activate_plugin() call returns, in the same request — has
// already run by the time we read bsf_product_referers. Reading this synchronously
// here would race that write, since this constructor can execute mid-request while
// SureForms itself is being activated by that other plugin.
if ( did_action( 'shutdown' ) ) {
$this->track_plugin_activated_event();
} else {
add_action( 'shutdown', [ $this, 'track_plugin_activated_event' ], PHP_INT_MAX );
}

// One-time: re-send onboarding_completed with full properties (v2).
if ( ! Helper::get_srfm_option( 'onboarding_event_v2_flushed', false )
Expand Down
17 changes: 17 additions & 0 deletions admin/assets/js/notice-response.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,23 @@
);
}

// Action-item notices declare what to record on the element itself, so a new
// one needs no entry in the map above. Delegated from the document because
// these render on every admin screen, not inside one known container.
document.addEventListener( 'click', function ( e ) {
const link = e.target.closest( '[data-srfm-notice-id][data-srfm-button]' );
if ( ! link ) {
return;
}

const noticeId = link.getAttribute( 'data-srfm-notice-id' );
const button = link.getAttribute( 'data-srfm-button' );

if ( noticeId && button ) {
sendResponse( noticeId, button );
}
} );

Object.keys( notices ).forEach( function ( noticeId ) {
const container = document.getElementById( noticeId );
if ( ! container ) {
Expand Down
13 changes: 11 additions & 2 deletions admin/notice-manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,10 @@ public function __construct() {
*
* @type string $id Required. Unique notice identifier.
* @type string $variant Notice type: 'error', 'warning', 'info', 'success'. Default 'info'.
* @type string $message Required. Notice message (can contain HTML).
* @type string $message Required. Notice message. Plain text only —
* AdminNotice.js renders it as a React child, so any
* markup shows up as literal characters. Links belong
* in `actions`.
* @type string $title Optional. Notice title.
* @type array $actions Optional. Array of action button configurations.
* @type bool $dismissible Optional. Whether notice can be dismissed. Default true.
Expand All @@ -73,6 +76,12 @@ public function __construct() {
* @type string $variant Optional. Button variant: 'primary', 'secondary', 'link'. Default 'primary'.
* @type string $size Optional. Button size: 'sm', 'md', 'lg'. Default 'sm'.
* @type string $className Optional. Additional CSS classes.
* @type string $action Optional. Opaque identifier that AdminNotice.js resolves
* to a local handler, for an action that must call the
* server. Deliberately NOT a URL, endpoint or HTTP method:
* the server never tells the browser which address to call.
* Pair it with `url` so the action still does something if
* no handler is registered for the identifier.
* }
*
* @return void
Expand Down Expand Up @@ -187,7 +196,7 @@ public function add_notices_to_localized_data( $localization_data ) {
*
* @param string $id Unique notice identifier.
* @param string $variant Notice type: 'error', 'warning', 'info', 'success'.
* @param string $message Notice message (HTML allowed).
* @param string $message Notice message. Plain text only; see register_notice().
* @param array $actions Optional. Array of action button configurations.
* @param array $pages Optional. Page slugs where notice should appear. Default ['all'].
* @return void
Expand Down
Loading