You may have noticed that when creating an occurrence in Arena, you can associate it with an area….
This is useful if you are providing Area specific pages on your web site and you’d like people to see any events that are occurring for that area, or you’d like your area leaders be able to add and edit events specific to their area.
The occurrence list module will display all occurrences specific to an area as long as you have the Area Filter Parameter value set…
This value is what the module will look for on the Query string to determine if it should just display occurrences for a specific area. Because the public Area page has “area” in the query string, placing this module on the page will display all occurrences for the current area.
You can also add a link to an area specific list in the Arena application by using the Tool tab in the Area Detail page.
A cool feature on the Area Details page of Arena is the ability to customize the "Tools" tab to include any HTML that you’d like. When editing the settings of the Area Detail module, any HTML that you include in the Details section will be displayed in the Tools tab.
For example, this the HTML that we’ve entered for our Area Details module…
<div class="heading2">Tools</div> <div class="normalText" style="padding-left:15px"> <a href='default.aspx?page=1437&Area=<area_id>'><area_name> Events</a><br/> <a href='default.aspx?page=4065&Area=<area_id>'>View <area_name> Families</a><br/> </div> <div class="heading2" style="padding-top:20px">Reports</div> <div class="normalText" style="padding-left:20px"> <a href='default.aspx?page=3987&title=<area_name_encoded> Members, Attenders and Guest Book Signers&AreaID=<area_id>' target='_blank'><area_name> Members, Attenders and Guest Book Signers</a><br/> </div>
As you can see in this example there are some codes you can enter that will be replaced with data from the current Area that is being displayed..
<area_id> will be replaced with the area’s ID
<area_name> will be replaced with the area’s name
<area_name_encoded> will be replaced with a URLEncoded version of the area’s name.
We use this functionality to link to area specific occurrences (next post), an Area specific person search page (using the AreaPersonFilter module), and to area specific custom reports.
The Metric functionality in Arena is a very powerful tool for viewing changes in data over time (yes, I know, the UI could still use some improvements). Essentially it allows you to create any number of metrics by specifying a SQL statement that just returns one numeric value. Arena will then run that statement how ever often you decide, save the result, and chart the values over time.
You can also set up a metric that isn’t linked to Arena data but instead is hand-entered (i.e. Total Weekend Attendance)
Membership Status Metrics
Since you may not have gotten any sample metrics out of the box, here’s a SQL script that will add a few for you…
DECLARE @OrganizationID int SET @OrganizationID = (SELECT TOP 1 organization_id FROM orgn_organization) DECLARE @AllUsers int SET @AllUsers = (SELECT TOP 1 role_id FROM secu_role WHERE role_name = 'All Users') DECLARE @ParentMetricID int DECLARE @MetricID int -- Create Parent Metric INSERT INTO [Arena].[dbo].[mtrc_metric] ([date_created] ,[date_modified] ,[created_by] ,[modified_by] ,[parent_metric_id] ,[metric_order] ,[graph_type] ,[title] ,[series_caption] ,[description] ,[source_summary] ,[collection_frequency] ,[collection_last_date] ,[collection_sql_statement] ,[organization_id] ,[aggregate_type] ,[numeric_x_values]) VALUES (GETDATE() ,GETDATE() ,'' ,'' ,NULL ,1 ,-1 ,'Membership Status' ,'' ,'Metric to show the number of people of each Membership Status over time' ,'' ,0 ,'1/1/1900' ,'' ,@OrganizationID ,'MAX' ,0) SET @ParentMetricID = SCOPE_IDENTITY() INSERT INTO [Arena].[dbo].[secu_permission] ([date_created] ,[date_modified] ,[created_by] ,[modified_by] ,[object_type] ,[operation_type] ,[subject_type] ,[object_key] ,[subject_key]) VALUES (GETDATE() ,GETDATE() ,'' ,'' ,6 ,0 ,0 ,@ParentMetricID ,@AllUsers ) INSERT INTO [Arena].[dbo].[secu_permission] ([date_created] ,[date_modified] ,[created_by] ,[modified_by] ,[object_type] ,[operation_type] ,[subject_type] ,[object_key] ,[subject_key]) VALUES (GETDATE() ,GETDATE() ,'' ,'' ,6 ,1 ,0 ,@ParentMetricID ,@AllUsers ) DECLARE @LookupID int DECLARE @LookupValue varchar(50) DECLARE @LookupOrder int DECLARE MemberStatusCursor CURSOR LOCAL FOR SELECT lookup_id, lookup_value, lookup_order FROM core_lookup L INNER JOIN core_lookup_type LT ON LT.lookup_type_id = L.lookup_type_id WHERE LT.guid = '0B4532DB-3188-40F5-B188-E7E6E4448C85' OPEN MemberStatusCursor FETCH NEXT FROM MemberStatusCursor INTO @LookupID, @LookupValue, @LookupOrder WHILE @@FETCH_STATUS = 0 BEGIN INSERT INTO [Arena].[dbo].[mtrc_metric] ([date_created] ,[date_modified] ,[created_by] ,[modified_by] ,[parent_metric_id] ,[metric_order] ,[graph_type] ,[title] ,[series_caption] ,[description] ,[source_summary] ,[collection_frequency] ,[collection_last_date] ,[collection_sql_statement] ,[organization_id] ,[aggregate_type] ,[numeric_x_values]) VALUES (GETDATE() ,GETDATE() ,'' ,'' ,@ParentMetricID ,@LookupOrder ,0 ,'Number of ' + @LookupValue + 's' ,@LookupValue + 's' ,'Number of people classifed as "' + @LookupValue + '"' ,'' ,2 ,GETDATE() ,'SELECT COUNT(*) AS people_count FROM Arena.dbo.core_person WHERE record_status = 0 AND member_status = ' + CAST(@LookupID as varchar) ,@OrganizationID ,'MAX' ,0) SET @MetricID = SCOPE_IDENTITY() INSERT INTO [Arena].[dbo].[secu_permission] ([date_created] ,[date_modified] ,[created_by] ,[modified_by] ,[object_type] ,[operation_type] ,[subject_type] ,[object_key] ,[subject_key]) VALUES (GETDATE() ,GETDATE() ,'' ,'' ,6 ,0 ,0 ,@MetricID ,@AllUsers ) INSERT INTO [Arena].[dbo].[secu_permission] ([date_created] ,[date_modified] ,[created_by] ,[modified_by] ,[object_type] ,[operation_type] ,[subject_type] ,[object_key] ,[subject_key]) VALUES (GETDATE() ,GETDATE() ,'' ,'' ,6 ,1 ,0 ,@MetricID ,@AllUsers ) FETCH NEXT FROM MemberStatusCursor INTO @LookupID, @LookupValue, @LookupOrder END CLOSE MemberStatusCursor DEALLOCATE MemberStatusCursor
Running this script will create a parent “Membership Status” metric and then a child metric for each Member Status you’ve defined (and grants the “All Users” role view and edit permissions).
Each of the child metrics will chart the number of people of that member status over time. As long as the ArenaProcessMetrics agent is running, you will see a chart after it’s collected the metrics data twice (two weeks from now).
Historical Data
If you don’t want to wait two weeks to see your data and/or you want to see all your historical member status data up to this point, we can use the core_member_status_history table to generate historical membership status data for your metrics…
SET NOCOUNT ON GO DECLARE @ParentMetricID int DECLARE @MinHistoryDate datetime DECLARE @WorkingDate datetime DECLARE @Status varchar(15) SET @ParentMetricID = (SELECT MAX(metric_id) FROM mtrc_metric WHERE title = 'Membership Status' AND parent_metric_id IS NULL) SET @MinHistoryDate = (SELECT MIN(date_created) FROM core_member_status_history) SET @WorkingDate = GETDATE() DELETE mtrc_metric_item FROM mtrc_metric_item MI INNER JOIN mtrc_metric M ON M.metric_id = MI.metric_id WHERE M.parent_metric_id = @ParentMetricID WHILE @MinHistoryDate IS NOT NULL AND @WorkingDate > @MinHistoryDate BEGIN SET @Status = CONVERT(varchar(10), @WorkingDate, 101) + '...' RAISERROR (@Status, 0, 1) WITH NOWAIT; WITH memberStatusTable (person_id, member_status) AS ( SELECT person_id, ISNULL((SELECT TOP 1 member_status FROM core_member_status_history WHERE person_id = P.person_id AND date_created < @WorkingDate ORDER BY date_created DESC), p.member_status) AS member_status FROM core_person P WHERE date_created <= @WorkingDate ) INSERT INTO mtrc_metric_item SELECT GETDATE() ,GETDATE() ,'' ,'' ,M.metric_id ,(SELECT COUNT(*) FROM memberStatusTable WHERE member_status = L.lookup_id) ,@WorkingDate ,'' ,0 FROM core_lookup L INNER JOIN core_lookup_type LT ON LT.lookup_type_id = L.lookup_type_id INNER JOIN mtrc_metric M ON M.title = 'Number of ' + L.lookup_value + 's' AND M.parent_metric_id = @ParentMetricID WHERE LT.guid = '0B4532DB-3188-40F5-B188-E7E6E4448C85' SET @WorkingDate = DATEADD(wk, -1, @WorkingDate) END
This script will create data for the metrics you created above for as far back as you have data in the core_member_status_history table.
In the past I’ve blogged about how to generate dynamic HTML content based on the query string using the “HTML From Stored Procedure” module. With the latest release of Arena (v 2008.1) the “Advanced HTML Text” module also has some smarts built into it to render content specific to the query string. We took advantage of this to provide an easy way for our finance department to produce a contribution statement for a member with any date range. We added an instance of the HTML text module to the top of the contributions tab on the person detail page with input boxes that allow the user to enter a date range…
If you look at the HTML, you can see that when the user clicks the “View Report” it is calling a Javascript function that opens a new window using the ReportPDFViewer page (this page renders a PDF version of any Reporting Services report, by passing parameters on the query string).
<script language="javascript">1:2: function showGivingReport()3: {4: var sDate = document.frmMain.tbAttendStart.value;5: var eDate = document.frmMain.tbAttendEnd.value;6: window.open('/Arena/ReportPDFViewer.aspx?Report=/Arena/Contributions/ContributionStatements_ccv&OrganizationId=3&FromDate=' +7: sDate + '&ToDate=' + eDate + '&FundID1=34&FundID2=56&MinimumGift=0&IncludePledgeWithoutGift=True&Chapter=1&PersonGUID=##GUID##');8: }</script>
<div class="normalText" style="padding-bottom: 5px;">
View contribution statement for the date range you specify below...
</div>
<table>
<tbody>
<tr>
<td class="formLabel">
Start Date/Time:
</td>
<td>
<input class="formItem" id="tbAttendStart" name="tbAttendStart">
</td>
<td class="smallText">
(mm/dd/yyy hh:mm am/pm)
</td>
</tr>
<tr>
<td class="formLabel">
End Date/Time:
</td>
<td>
<input class="formItem" id="tbAttendEnd" name="tbAttendEnd">
</td>
<td class="smallText">
(mm/dd/yyy hh:mm am/pm)
</td>
</tr>
<tr>
<td>
<br>
</td>
<td style="padding-left: 3px; padding-top: 4px;" colspan="2">
<button class="smallText" onclick="showGivingReport()">
View Report</button>
</td>
</tr>
</tbody>
</table>
<br>
<br>
We hard code some of the parameters like the report name, funds, etc., but the PersonGUID parameter needs to vary based on the person record that we’re currently viewing.
To accomplish this, we include the “##GUID##” text. The Advanced HTML Text module then evaluates the query string and looks for each of the query string parameters in the HTML content surrounded by the ## characters. Because GUID is a query string parameter on the person detail page, the ##GUID## characters will be replaced with the person’s GUID value from the query string when rendering the HTML.
For this to work, you need to set a new setting on the Advanced HTML Text module…
Arena will only evaluate the query string when this setting is set to True (the default is False).
Something that has helped us make considerable performance improvements in Arena is the use of Common Table Expressions. Common Table Expressions are especially useful when we need a list of hierarchal data. For example below are two stored procedures that were previously used to get a list of tag members in a hierarchal list of tags (this procedures is used by the Volunteer Administration page under Volunteer Tracking).
IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[core_sp_get_profile_member_Details]') AND type in (N'P', N'PC')) DROP PROCEDURE [dbo].[core_sp_get_profile_member_Details] SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO CREATE PROC dbo.core_sp_get_profile_member_Details @OrganizationID int, @ProfileType int, @OwnerID int, @FirstName varchar(40), @LastName varchar(40), @ParentProfileID int AS CREATE TABLE #ChildProfiles( [profile_id] [int] NOT NULL) -- Declare a cursor for determinining the groups and members in any child profiles DECLARE @ProfileID int DECLARE @ProfileOwnerID int DECLARE ChildProfile CURSOR LOCAL FOR SELECT profile_id, owner_id FROM core_profile WHERE organization_id = @OrganizationID AND profile_type = @ProfileType AND @ParentProfileID = -1 OR (parent_profile_id = @ParentProfileID OR profile_id = @ParentProfileID) -- Open the cursor OPEN ChildProfile -- Fetch the first record FETCH NEXT FROM ChildProfile INTO @ProfileID, @ProfileOwnerID -- Loop through all the records WHILE @@FETCH_STATUS = 0 BEGIN IF @ProfileType <> 0 OR @ProfileOwnerID = @OwnerID BEGIN INSERT INTO #ChildProfiles VALUES (@ProfileID) -- For each child profile, call this stored procedure recursively to recurse down -- all child profiles. DECLARE @RC int EXEC @RC = core_sp_get_profile_member_Details_Recurse @ProfileID END -- Read the next record FETCH NEXT FROM ChildProfile INTO @ProfileID, @ProfileOwnerID END -- Close and deallocate the cursor CLOSE ChildProfile DEALLOCATE ChildProfile SELECT * FROM core_v_profile_memberList WHERE profile_id in (SELECT profile_id FROM #ChildProfiles) AND (nick_name like @FirstName + '%' OR first_name like @FirstName + '%') AND (last_name like @LastName + '%') ORDER BY person_name DROP TABLE #ChildProfiles
USE [Arena] GO /****** Object: StoredProcedure [dbo].[core_sp_get_profile_member_Details_Recurse] Script Date: 04/29/2008 19:05:32 ******/ SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO ALTER PROC [dbo].[core_sp_get_profile_member_Details_Recurse] @ParentProfileID int AS -- Declare a cursor for determinining the groups and members in any child profiles DECLARE @ProfileID int DECLARE ChildProfile CURSOR LOCAL FOR SELECT profile_id FROM core_profile WHERE isnull(parent_profile_id, -1) = @ParentProfileID -- Open the cursor OPEN ChildProfile -- Fetch the first record FETCH NEXT FROM ChildProfile INTO @ProfileID -- Loop through all the records WHILE @@FETCH_STATUS = 0 BEGIN INSERT INTO #ChildProfiles VALUES (@ProfileID) -- For each child profile, call this stored procedure recursively to recurse down -- all child profiles. DECLARE @RC int EXEC @RC = core_sp_get_profile_member_Details_Recurse @ProfileID -- Read the next record FETCH NEXT FROM ChildProfile INTO @ProfileID END -- Close and deallocate the cursor CLOSE ChildProfile DEALLOCATE ChildProfile
You can see in these stored procedures that we were using a temporary table, a cursor, and then calling another stored procedure recursively, all to get a list of child tags in a hierarchy. With Common Table Expressions we were able to remove all of this. Below is the new procedure…
USE [Arena] GO /****** Object: StoredProcedure [dbo].[core_sp_get_profile_member_Details] Script Date: 04/29/2008 18:57:30 ******/ SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO ALTER PROC [dbo].[core_sp_get_profile_member_Details] @OrganizationID int, @ProfileType int, @OwnerID int, @FirstName varchar(40), @LastName varchar(40), @ParentProfileID int, @Status varchar(500) AS WITH cte_profiles ( [profile_id] ) AS ( SELECT P.[profile_id] FROM core_profile P WHERE (ISNULL(P.parent_profile_id,-1) = @ParentProfileID OR P.profile_id = @ParentProfileID) AND P.organization_id = @OrganizationID AND P.profile_type = @ProfileType AND (P.profile_type <> 0 OR p.owner_id = @OwnerID) UNION ALL SELECT P.[profile_id] FROM core_profile P INNER JOIN cte_profiles CTE ON CTE.profile_id = ISNULL(P.parent_profile_id,-1) WHERE P.organization_id = @OrganizationID AND P.profile_type = @ProfileType AND (P.profile_type <> 0 OR p.owner_id = @OwnerID) ) SELECT ML.* FROM cte_profiles P INNER JOIN core_profile_member PM ON PM.profile_id = P.profile_id INNER JOIN core_v_profile_memberList ML ON ML.profile_id = PM.profile_id AND ML.person_id = PM.person_id WHERE (ML.nick_name like @FirstName + '%' OR ML.first_name like @FirstName + '%') AND (ML.last_name like @LastName + '%') AND CAST(PM.status_luid as varchar) in ( SELECT item FROM dbo.fnSplit(@Status) ) ORDER BY ML.last_name, ML.nick_name
The new procedure does a recursive union within the Common Table Expression. Using this method is incredibly faster than the original cursor.
I’ve gotten a few questions about how Arena determines the appropriate portal to load when someone requests an Arena site. Here’s the steps that Arena goes through to determine which portal should be loaded…
- The first thing it checks is if a “portal” query string parameter was specified. If so, it loads the portal with the specified ID.
- It will then check for an existing cookie that may have been saved by a previous request during the same browser session (If this is the first request during the the user’s browser session, this cookie will not exist).
- If a “page” query string parameter was specified, Arena will find that page in the page hierarchy and then find it’s very topmost (root) page. If the root page happens to be the default page for any of the portals, then the first portal with that default page will be loaded.
- It will then read the SERVER_NAME header on the request and check each portal to see if the SERVER_NAME header contains the “Default Domain” value specified for the portal. The first portal with a Default Domain value that is present in the SERVER_NAME header will be loaded.
- If a portal has not been loaded by any of the previous methods, Arena will load the portal specified by the “DefaultPortalId” value specified in the Web.Config file.
After loading a portal, Arena saves the portal as a session cookie, so that it can use step 2 above on each of the user’s following requests.
Keep in mind, all this is done once the request makes it to the Arena application. When debugging an issue related to what portal is getting loaded, make sure to consider other factors too, such as your IIS configuration, or other ASP pages that may be redirecting the user. That’s just to say that the URL you see in your browser window, may not be the URL that Arena sees when it is attempting to determine the appropriate portal to load.
I recently needed to delete all the Arena images from the util_blob table in a copy of our production database. Doing a DELETE would take too long since it’s a logged transaction and we have Gigs of stored image data. Using TRUNCATE is what I wanted to do since it’s not a logged transaction and would quickly delete all the rows. Problem is that you can’t use TRUNCATE if the table has any foreign key relationships and the util_blob table has several.
Since this is a common task for me, I wanted to create a script that would drop all the relationships, TRUNCATE the table, and then rebuild the relationships. Enter the SQL System Views which will tell you anything you’d like to know about the structure of your database.
The script below uses these System Views to determine all of the table’s that have a foreign key relationship with util_blob. It then set’s the related field on those tables to null, drops the relationship, does the TRUNCATE, and then rebuilds the relationships, and it will continue to work even when additional relationships are added.
After running this script and shrinking the database it went from over 24 GB to just under 2 GB which is a lot nicer to work with on my small development machine. As a bonus I also got to learn the importance of making sure I know the difference between production and a copy of production when staff started asking what happened to all the images in Arena.
Here’s the script. Make sure not to run it on your production database…
BEGIN TRANSACTION SET NOCOUNT ON DECLARE @ConstraintName varchar(128) DECLARE @IsDisabled bit DECLARE @DeleteAction varchar(128) DECLARE @TableName varchar(128) DECLARE @ColumnName varchar(128) DECLARE @SqlStatement varchar(4000) -- Create a temporary table for storing the SQL statements to recreate the relationships CREATE TABLE #ConstraintTable ([create_contraint] varchar(4000)) -- Create a cursor to spin through all the relationships with the blob_id field on the util_blob table DECLARE RelationshipCursor INSENSITIVE CURSOR FOR SELECT k.name AS constraint_name, k.is_disabled, CASE k.delete_referential_action WHEN 2 THEN 'SET NULL' ELSE 'NO ACTION' END, tso.name, tac.name FROM sys.foreign_key_columns kc INNER JOIN sys.foreign_keys k ON kc.constraint_object_id = k.object_id INNER JOIN sys.all_objects so ON so.object_id = kc.referenced_object_id INNER JOIN sys.all_columns rac ON rac.column_id = kc.referenced_column_id AND rac.object_id = so.object_id INNER JOIN sys.all_objects tso ON tso.object_id = kc.parent_object_id INNER JOIN sys.all_columns tac ON tac.column_id = kc.parent_column_id AND tac.object_id = tso.object_id WHERE so.name = 'util_blob' AND rac.name = 'blob_id' OPEN RelationshipCursor FETCH NEXT FROM RelationshipCursor INTO @ConstraintName, @IsDisabled, @DeleteAction, @TableName, @ColumnName WHILE (@@FETCH_STATUS <> -1) BEGIN IF (@@FETCH_STATUS = 0) BEGIN -- Update the foreign table's field value to NULL SET @SqlStatement = 'UPDATE ' + @TableName + ' SET ' + @ColumnName + '= null' EXEC (@SqlStatement) -- Drop the relationship SET @SqlStatement = 'ALTER TABLE ' + @TableName + ' DROP CONSTRAINT ' + @ConstraintName EXEC (@SqlStatement) -- Create the SQL statement to recreate the relationship and save it to the temporary table INSERT INTO #ConstraintTable VALUES ( 'ALTER TABLE ' + @TableName + ' ADD ' + CHAR(13) + CHAR(10) + ' CONSTRAINT ' + @ConstraintName + ' FOREIGN KEY(' + @ColumnName + ') REFERENCES util_blob(blob_id) ON DELETE ' + @DeleteAction + CHAR(13) + CHAR(10) + CHAR(13) + CHAR(10) ) IF @IsDisabled = 1 BEGIN INSERT INTO #ConstraintTable VALUES ( 'ALTER TABLE ' + @TableName + CHAR(13) + CHAR(10) + ' NOCHECK CONSTRAINT ' + @ConstraintName + CHAR(13) + CHAR(10) + CHAR(13) + CHAR(10) ) END END FETCH NEXT FROM RelationshipCursor INTO @ConstraintName, @IsDisabled, @DeleteAction, @TableName, @ColumnName END CLOSE RelationshipCursor DEALLOCATE RelationshipCursor -- Truncate the table TRUNCATE TABLE util_blob -- Create a cursor to spin through all the create statements DECLARE ConstraintCursor INSENSITIVE CURSOR FOR SELECT create_contraint FROM #ConstraintTable OPEN ConstraintCursor FETCH NEXT FROM ConstraintCursor INTO @SqlStatement WHILE (@@FETCH_STATUS <> -1) BEGIN IF (@@FETCH_STATUS = 0) BEGIN -- Execute the SQL statement to recreate the relationship. EXEC (@SqlStatement) END FETCH NEXT FROM ConstraintCursor INTO @SqlStatement END CLOSE ConstraintCursor DEALLOCATE ConstraintCursor DROP TABLE #ConstraintTable -- Update the statistics on the table since all the records have been deleted UPDATE STATISTICS util_blob SET NOCOUNT OFF COMMIT TRANSACTION
If you’ve attempted to create Geographic Areas in Arena, you know it’s a difficult process. Fortunately there’s an easier way. Using Microsoft MapPoint, you can draw freeform shapes on a map, and then use a new utility to import those shapes into Arena as areas. Below is a video that shows how to do this.
The video content presented here requires JavaScript to be enabled and the latest version of the Macromedia Flash Player. If you are you using a browser with JavaScript disabled please enable it now. Otherwise, please update your version of the free Flash Player by downloading here.
The Import MapPoint Areas utility can be downloaded from the “Arena Shared Files” folder in the Downloads section of the Arena Community site.
If you’ve configured Arena to email you every exception that occurs in Arena, you may get tired of seeing the exceptions that occur when a search bot crawls your site. I certainly do. Fortunately, you can configure Arena so that it ignores exceptions based on the value of the HTTP_USER_AGENT http header.
When an exception occurs, Arena will evaluate the current HTTP_USER_AGENT value, and if it contains any of the values you’ve defined in the “ExceptionUserAgentIgnore” organization setting, then the exception email will not be sent.
Here’s the current value of our “ExceptionUserAgentIgnore” setting…
msnbot;Slurp;CCV Search;Googlebot;gsa-crawler;ia_archiver;BusyBot;Gigabot;MJ12bot;PycURL;ScanAlert;exabot;singingfish;becomebot;converacrawler;twiceler;crawler;WebCopier
Since there always seems to be a new search bot created, you may need to periodically add new values to this org setting. You’ll now when you begin to get a flurry of new exceptions reported.
Jon suggested I create a quick video that demonstrates how easy it is to create, register, and use a custom module in Arena. Caleb Tucker did a similar presentation at last month’s Developer Roundtable, but if you missed that, then here’s a video that walks you through the process and shows some of the Arena framework that can by utilized by your custom module…
If you’re going to create a custom module, make sure to download the Arena Custom Module Development guide from the Arena Community Site.
