Skip to content

Commit

Permalink
- (Event) Added exception logging to help identify problem area(s) an…
Browse files Browse the repository at this point in the history
…d a possible solution to prevent future event registrations from coming through with missing registrant data. (#5091)
  • Loading branch information
jasonhendee committed Aug 16, 2023
1 parent 42c3d98 commit e02749c
Show file tree
Hide file tree
Showing 6 changed files with 434 additions and 0 deletions.
116 changes: 116 additions & 0 deletions Rock.Blocks/Event/RegistrationEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,15 @@ private static class PageParameterKey

#endregion Keys

#region Properties

/// <summary>
/// A diagnostic collection of missing fields, grouped by form ID.
/// </summary>
public Dictionary<int, Dictionary<int, string>> MissingFieldsByFormId { get; set; }

#endregion Properties

#region Obsidian Block Type Overrides

/// <summary>
Expand Down Expand Up @@ -714,6 +723,36 @@ private RegistrationSession UpsertSession(
/// <exception cref="Exception">There was a problem with the payment</exception>
private Registration SubmitRegistration( RockContext rockContext, RegistrationContext context, RegistrationEntryBlockArgs args, out string errorMessage )
{
/*
8/15/2023 - JPH
In order to successfully save the registration form values that were provided by the registrar, we must
have each [RegistrationTemplateForm].[Fields] collection loaded into memory below. Several individuals have
reported seeing missing registrant data within completed registrations, so it's possible that these Fields
collections are somehow empty.
The TryLoadMissingFields() method is a failsafe to ensure we have the data we need to properly save the
registration. This method will:
1) Attempt to load any missing Fields collections;
2) Return a list of any Form IDs that were actually missing Fields so we can log them to prove that
this was a likely culprit for failed, past registration attempts (and so we can know to look into
the issue further from this angle).
Reason: Registration entries are sometimes missing registration form data.
https://github.com/SparkDevNetwork/Rock/issues/5091
*/
var logInstanceOrTemplateName = context?.RegistrationSettings?.Name;
var logCurrentPersonDetails = $"Current Person Name: {this.RequestContext.CurrentPerson?.FullName} (Person ID: {this.RequestContext.CurrentPerson?.Id});";
var logMsgPrefix = $"Obsidian{( logInstanceOrTemplateName.IsNotNullOrWhiteSpace() ? $@" ""{logInstanceOrTemplateName}""" : string.Empty )} Registration; {logCurrentPersonDetails}{Environment.NewLine}";

var ( wereFieldsMissing, missingFieldsDetails ) = new RegistrationTemplateFormService( rockContext ).TryLoadMissingFields( context?.RegistrationSettings?.Forms );
if ( wereFieldsMissing )
{
var logMissingFieldsMsg = $"{logMsgPrefix}RegistrationTemplateForm(s) missing Fields data when trying to save Registration.{Environment.NewLine}{missingFieldsDetails}";

ExceptionLogService.LogException( new RegistrationTemplateFormFieldException( logMissingFieldsMsg ) );
}

errorMessage = string.Empty;
var currentPerson = GetCurrentPerson();

Expand Down Expand Up @@ -971,6 +1010,8 @@ private Registration SubmitRegistration( RockContext rockContext, RegistrationCo
var forceWaitlist = context.SpotsRemaining < 1 && ( isNewRegistration == true || registrantInfo.IsOnWaitList == true );
bool isCreatedAsRegistrant = context.RegistrationSettings.RegistrarOption == RegistrarOption.UseFirstRegistrant && registrantInfo == args.Registrants.FirstOrDefault();

MissingFieldsByFormId = new Dictionary<int, Dictionary<int, string>>();

UpsertRegistrant(
rockContext,
context,
Expand All @@ -986,6 +1027,37 @@ private Registration SubmitRegistration( RockContext rockContext, RegistrationCo
postSaveActions );

index++;

if ( MissingFieldsByFormId?.Any() == true )
{
/*
8/15/2023 - JPH
Several individuals have reported seeing missing registrant data within completed registrations. This registrant
is missing required, non-conditional Field value(s) that should have been enforced by the UI. Log an exception so
we know which values were missing during the saving of this registrant's data (and so we can know to look into
the issue further from this angle).
Reason: Registration entries are sometimes missing registration form data.
https://github.com/SparkDevNetwork/Rock/issues/5091
*/
var logAllMissingFieldsSb = new StringBuilder();
logAllMissingFieldsSb.AppendLine( $"{logMsgPrefix}Registrant {index} of {args.Registrants.Count}: The following required (non-conditional) Field values were missing:" );

foreach ( var missingFormFields in MissingFieldsByFormId )
{
var logMissingFormFieldsSb = new StringBuilder( $"[Form ID: {missingFormFields.Key} -" );

foreach ( var missingField in missingFormFields.Value )
{
logMissingFormFieldsSb.Append( $" {missingField.Value} (Field ID: {missingField.Key});" );
}

logAllMissingFieldsSb.AppendLine( $"{logMissingFormFieldsSb}]" );
}

ExceptionLogService.LogException( new RegistrationTemplateFormFieldException( logAllMissingFieldsSb.ToString() ) );
}
}

rockContext.SaveChanges();
Expand Down Expand Up @@ -1652,6 +1724,44 @@ private void SavePhone( object fieldValue, Person person, Guid phoneTypeGuid, Hi
var birthday = GetPersonFieldValue( context.RegistrationSettings, RegistrationPersonFieldType.Birthdate, registrantInfo.FieldValues ).ToStringSafe().FromJsonOrNull<BirthdayPickerBag>().ToDateTime();
var mobilePhone = GetPersonFieldValue( context.RegistrationSettings, RegistrationPersonFieldType.MobilePhone, registrantInfo.FieldValues ).ToStringSafe();

/*
8/15/2023 - JPH
Several individuals have reported seeing missing registrant data within completed registrations. Check
each person field type to see if it was required, non-conditional & missing, so we know whether to look
into the issue further from this angle.
Reason: Registration entries are sometimes missing registration form data.
https://github.com/SparkDevNetwork/Rock/issues/5091
*/
if ( MissingFieldsByFormId != null )
{
void NotePersonFieldDetailsIfRequiredAndMissing( RegistrationPersonFieldType personFieldType, object fieldValue )
{
var field = context.RegistrationSettings
?.Forms
?.SelectMany( f => f.Fields
.Where( ff =>
ff.FieldSource == RegistrationFieldSource.PersonField
&& ff.PersonFieldType == personFieldType
)
).FirstOrDefault();

if ( field == null )
{
return;
}

field.NoteFieldDetailsIfRequiredAndMissing( MissingFieldsByFormId, fieldValue );
}

NotePersonFieldDetailsIfRequiredAndMissing( RegistrationPersonFieldType.FirstName, firstName );
NotePersonFieldDetailsIfRequiredAndMissing( RegistrationPersonFieldType.LastName, lastName );
NotePersonFieldDetailsIfRequiredAndMissing( RegistrationPersonFieldType.Email, email );
NotePersonFieldDetailsIfRequiredAndMissing( RegistrationPersonFieldType.Birthdate, birthday );
NotePersonFieldDetailsIfRequiredAndMissing( RegistrationPersonFieldType.MobilePhone, mobilePhone );
}

registrant = context.Registration.Registrants.FirstOrDefault( r => r.Guid == registrantInfo.Guid );

if ( registrant != null )
Expand Down Expand Up @@ -1936,6 +2046,8 @@ private void SavePhone( object fieldValue, Person person, Guid phoneTypeGuid, Hi
}
}
}

field.NoteFieldDetailsIfRequiredAndMissing( MissingFieldsByFormId, fieldValue );
}

return (campusId, location, updateExistingCampus);
Expand Down Expand Up @@ -1993,6 +2105,8 @@ private bool UpdatePersonAttributes( Person person, History.HistoryChangeList pe
}
}
}

field.NoteFieldDetailsIfRequiredAndMissing( MissingFieldsByFormId, fieldValue );
}

return isChanged;
Expand Down Expand Up @@ -2310,6 +2424,8 @@ private bool UpdateRegistrantAttributes( RegistrationRegistrant registrant, View
isChanged = true;
History.EvaluateChange( registrantChanges, attribute.Name, formattedOriginalValue, formattedNewValue );
}

field.NoteFieldDetailsIfRequiredAndMissing( MissingFieldsByFormId, newValue );
}

return isChanged;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// <copyright>
// Copyright by the Spark Development Network
//
// Licensed under the Rock Community License (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.rockrms.com/license
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// </copyright>
//

using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;

using Rock.Attribute;
using Rock.Data;

namespace Rock.Model
{
public partial class RegistrationTemplateFormService
{
/// <summary>
/// Tries to load fields from the database for any registration template forms that are missing fields.
/// <para>
/// "Missing" in this context means the <see cref="RegistrationTemplateForm.Fields"/> collection is <see langword="null"/> or empty.
/// </para>
/// </summary>
/// <param name="forms">The forms whose fields should be loaded if missing.</param>
/// <returns>A Tuple indicating if any forms were missing fields, and more details if so.</returns>
[RockInternal( "1.15.2" )]
public ( bool wereFieldsMissing, string details ) TryLoadMissingFields( IList<RegistrationTemplateForm> forms )
{
var formIds = ( forms ?? new List<RegistrationTemplateForm>() )
.Where( f => f.Fields?.Any() != true && f.Id > 0 )
.Select( f => f.Id )
.ToList();

if ( !formIds.Any() )
{
return ( false, null );
}

var preLoadMessage = $"{formIds.Count} RegistrationTemplateForm(s) missing Fields (Form IDs: {formIds.AsDelimited( ", " )}).";

// Try to load the fields from the database in bulk.
var fieldsByFormId = new RegistrationTemplateFormFieldService( Context as RockContext )
.Queryable()
.Include( f => f.Attributes )
.Where( f => formIds.Contains( f.RegistrationTemplateFormId ) )
.GroupBy( f => f.RegistrationTemplateFormId )
.ToDictionary( g => g.Key, g => g.ToList() );

foreach ( var formFields in fieldsByFormId )
{
var form = forms.FirstOrDefault( f => f.Id == formFields.Key );
if ( form != null )
{
form.Fields = formFields.Value;
}
}

// Re-check to see if we successfully loaded the fields for all forms.
formIds = forms.Where( f => f.Fields?.Any() != true && f.Id > 0 )
.Select( f => f.Id )
.ToList();

var formCount = formIds.Count;
var formsRemainString = formCount == 0
? "all Forms now have Fields."
: $"{formCount} Form(s) still missing Fields (Form IDs: {formIds.AsDelimited( ", " )}).";

var postLoadMessage = $"After attempting to load Fields, {formsRemainString}";

return ( true, $"{preLoadMessage} {postLoadMessage}" );
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,13 @@
// limitations under the License.
// </copyright>

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity;
using System.Linq;
using System.Text;

using Rock.Attribute;
using Rock.Web.Cache;

namespace Rock.Model
Expand Down Expand Up @@ -56,6 +61,71 @@ public void UpdateCache( EntityState entityState, Rock.Data.DbContext dbContext

#region Methods

/// <summary>
/// Adds a note identifying this field to the provided <paramref name="missingFieldsByFormId"/> collection if this field is
/// required, non-conditional and missing a value.
/// </summary>
/// <param name="missingFieldsByFormId">The collection of field details to which to add a note.</param>
/// <param name="fieldValue">The value - if any - that was provided for this field.</param>
[RockInternal( "1.15.2" )]
public void NoteFieldDetailsIfRequiredAndMissing( Dictionary<int, Dictionary<int, string>> missingFieldsByFormId, object fieldValue )
{
/*
8/15/2023 - JPH
Several individuals have reported seeing missing registrant data within completed registrations. This helper method was added
to make it easier to take note of any required, non-conditional Field values that should have been enforced by the UI.
Note that this method will NOT take into consideration any Fields that have visibility rules, and are therefore conditional.
To do so would require the aggregation and processing of more data. The goal - instead - is to quickly spot if there are
scenarios in which the always-required fields are somehow not being passed back to the server for saving, so we know whether
to look into the issue further from this angle.
Reason: Registration entries are sometimes missing registration form data.
https://github.com/SparkDevNetwork/Rock/issues/5091
*/
if ( missingFieldsByFormId == null
|| !this.IsRequired // Field is not required.
|| this.Id <= 0 // No ID to report; not helpful.
|| this.RegistrationTemplateFormId <= 0 // No form ID to report; not helpful.
|| this.FieldVisibilityRules?.RuleList?.Any() == true // This field is conditional; not enough info to determine if it's currently required.
|| fieldValue.ToStringSafe().IsNotNullOrWhiteSpace() // Field has a value (of some kind).
)
{
return;
}

// Find or add the parent form's collection.
missingFieldsByFormId.TryGetValue( this.RegistrationTemplateFormId, out Dictionary<int, string> formFields );
if ( formFields == null )
{
formFields = new Dictionary<int, string>();
missingFieldsByFormId.AddOrReplace( this.RegistrationTemplateFormId, formFields );
}

// Get the field details based on field source.
var detailsSb = new StringBuilder( $"{this.FieldSource.ConvertToString()}: " );

if ( this.FieldSource == RegistrationFieldSource.PersonField )
{
detailsSb.Append( this.PersonFieldType.ConvertToString() );
}
else
{
if ( this.Attribute == null )
{
detailsSb.Append( "Error - Attribute property is not defined" );
}
else
{
detailsSb.Append( this.Attribute.Name );
}
}

// Add or replace this field's value.
formFields.AddOrReplace( this.Id, detailsSb.ToString() );
}

/// <summary>
/// Returns a <see cref="string"/> that represents this instance.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// <copyright>
// Copyright by the Spark Development Network
//
// Licensed under the Rock Community License (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.rockrms.com/license
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// </copyright>
//

using System;

using Rock.Attribute;

namespace Rock.Model
{
/// <summary>
/// Exception to log if a registration form is unexpectedly missing fields, values, Etc.
/// </summary>
[RockInternal( "1.15.2" )]
public class RegistrationTemplateFormFieldException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="RegistrationTemplateFormFieldException"/> class.
/// </summary>
/// <param name="message">The message that describes the error.</param>
public RegistrationTemplateFormFieldException( string message ) : base( message )
{
}
}
}
2 changes: 2 additions & 0 deletions Rock/Rock.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -1037,6 +1037,8 @@
<Compile Include="Model\Event\InteractiveExperience\InteractiveExperience.cs" />
<Compile Include="Model\Event\InteractiveExperience\InteractiveExperience.Logic.cs" />
<Compile Include="Model\Event\InteractiveExperience\InteractiveExperienceService.cs" />
<Compile Include="Model\Event\RegistrationTemplateFormField\RegistrationTemplateFormFieldException.cs" />
<Compile Include="Model\Event\RegistrationTemplateForm\RegistrationTemplateFormService.cs" />
<Compile Include="Model\Group\GroupMember\GroupMemberService.WebForms.cs" />
<Compile Include="Model\Event\InteractiveExperienceScheduleCampus\InteractiveExperienceScheduleCampus.cs" />
<Compile Include="Model\Group\GroupMemberRequirement\GroupMemberRequirementService.cs" />
Expand Down
Loading

0 comments on commit e02749c

Please sign in to comment.