Travelling the World

Archive Page 2

Sometimes you might want to setup a Google Analytics Goal that is actually a link to an off-site url. For example, you conversion page might be on a different domain, but you want to be able to track users who click though to it as completing a goal.

We recently launched a Wordpress MU blog network called Au Pair News and wanted to track when people click on certain off-site links (such as our “View Available Au Pairs” page) as having completed a goal. Google Analytics allows you to do this with a 2 step process.

Step 1: Track a Page View For Your Links
Even though your link is off-site you can still track a virtual page view for it by using the pageTracker call. So for example, we might have:

<li><a href="http://www.culturalcare.com/hostfamilies/availableaupairs/" onclick="pageTracker._trackPageview('/avaaupairs/');">View Available Au Pairs</a></li>

Google will now track the /avaaupairs/ page in my aupairnews.com analytics account. (Learn more about trackPageview)

Step 2: Setup your Goal
Now that /avaaupairs/ is showing up as one of the content pages in Analytics, we can create a goal based off it. Because we’re operating a blog network where the subdomain is different for each user, our goal is setup as a Regular Expression with the goal url /avaaupairs/ (but if you know the full path, you could use an exact match or a head match as well).

Yahoo small business domains recently increased their yearly domain renewal fees to $30 (as compared to $7 for some others)

I needed to transfer several production domains from yahoo to godaddy and avoid downtime. Domain transfers are notoriously lengthy and painful; domain unlocking, transfer initiation, secret codes, and waiting, waiting, and more waiting.

Here’s a little tip to avoid downtime:

When you are transferring your domain, select to keep the existing DNS servers. This means that you don’t need to worry about re-configuring the DNS entries right away. First, your transfer will take place, and then, when you are good and ready you can change the name servers to godaddy (or your new registrar), and add all the appropriate entries in one sitting.

We have the Umbraco CMS (version 3) configured to use TinyMCE for WYSIWYG editing. It works quite well up until we want to insert some HTML content that TinyMCE strips out (like an embedded youtube video, or a screencast)

Here’s how to create a very simple Umbraco macro (using a c# user control) to allow you to insert arbitrary HTML code that won’t interfere with TinyMCE:

Create a visual studio solution and create InsertHTML.ascx with the following code:

< %@ Control Language="C#" AutoEventWireup="true" Codebehind="InsertHTML.ascx.cs"
    Inherits="YourNameSpace.InsertHTML" %>
< %=HTMLCode%>

The code-behind InsertHTML.ascx.cs just needs to have:

using System;
using System.Collections;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
 
namespace YourNameSpace
{
    public partial class InsertHTML : System.Web.UI.UserControl
    {
        private string __HTMLCode = "";
 
        public string HTMLCode
        {
            get
            {
                return __HTMLCode;
            }
            set
            {
                __HTMLCode = value;
            }
        }
 
    }
}

Build your DLL, drop it into the umbraco bin folder, and put the .ascx file in the \usercontrols\ directory. You’re almost done. Go to Developer -> Macros and create a new macro called “Insert HTML.” Point it to the user control on the server and be sure to check “use in editor.” Lastly, on the “Parameters” tab add a textMultiLine parameter and call it HTMLCode.

You should now see the “Insert HTML” macro as one of the options when you click the macro button in the TinyMCE editor. And we’re done.

Here’s a solution file extracted from my project:
Download Solution

Yucatan Photos

Yucatan photos are up. Check ‘em out.

Where to, chief?

So I just launched http://www.wheretochief.com. It’s a little silly, but try it out.

Mostly its is a technology demo:

  • Entirely Client Side
  • ZVents API (JSON via JQuery)
  • Facebook Connect
  • Blueprint CSS
  • UserVoice

Watcha think?

Ok, so you’re working with Google App Engine, cool.

It comes time to deploy your application, and of course you’ve created several application copies in your GAE account to have DEV, QA and PROD versions of your app. But how do you manage the app.yaml file which holds the application identifier? (this determines which version is uploaded).

Here’s a simple solution; I have an “upload DEV.bat” file in my root folder that has this simple code:

copy appDEV.yaml app.yaml
appcfg.py update .

When I double click on this file to update my DEV app the script just copies the DEV yaml file to app.yaml and then performs the GAE update. You could replicate this file for every environment you have. Presto.

Update: we’ve had some problems where some incorrect tables have been deleted by this script. We haven’t had time to investigate, but for now I don’t recommend using this script as is.

Elad posted a small script to delete inactive WordressMU blogs. This can be useful if you run a blog network and want to automate the removal of stale content and regain some database space. I recommend backing up your DB regularly if you plan on running this script in production as it deletes data from the DB.

AJ and I made some modification and improvements to Elad’s version. Namely:

  • fixed bugs in the original (swapped blogs_mail and blogs_delete values)
  • added option to exclude certain blogs from the process (so that you don’t accidentally delete your primary blog for example)
  • changed mail to use SMTP in case you don’t run a mail server on your WordpressMU box
  • changed delete procedure to loop though all user tables (no longer hard-coded to only certain tables)

To use this code place the .php file somewhere inside your wordpressMU install and setup a cron job to run it weekly (for example, something like “php -q /path/wpmu-cleanup.php”). Note that this code does not delete the user from the wp_users table, nor does it delete their folder in /wp-content/blogs.dir.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
< ?php
/**********************************************************
WPMU Cleanup
Original Version by Elad Salomons: http://www.ringofblogs.com/2008/06/10/wpmu-cleanup-script-to-delete-unused-blogs/
Modified by Boris Masis: http://www.borism.net/2008/12/31/deleting-inactive-wordpressmu-blogs
 
This script checks the WPMU database for blogs that have
not been updated for X days, then sends a notice to the
blogowner that the blog will be deleted after Y days.
 
It also automatically deletes blogs that have not been
updated for Y days, then sends an email to the blogowner.
 
On both occasions, the script will send a report with
details on warned and deleted blogs to the administrator.
 
This script has *NOT* been programmed as a plugin since
it is intended to be executed by a cronjob. As it is now
it should be run once every 7 days, but you can configure
it to do otherwise.
 
This script is *NOT* a final version and should be used
with caution and only if you know what you are doing. I
recommend testing it on a testsite rather than a livesite.
 
**********************************************************/
 
/* SETTINGS */
require_once "Mail.php";
require_once('wp-load.php');
$mysql_server				= '';
$mysql_username				= '';
$mysql_password				= '';
$mysql_database				= 'wordpress';
 
$options['email_host']		= '';
$options['email_from']		= ''; // Email-address used for sending notices.
$options['email_subject']	= 'Your Website';
$options['mail_report']		= ''; // Reports will be sent to this address.
$options['timetomail']		= 256; // Number of days blog can be inactive before receiving deletion warning.
$options['timetodeletion']	= 270; // Number of days blog can be inactive before deletion.
$options['dateformat']		= 'm-d-Y'; // Dateformat used in emails.
 
$options['noticemessage']	= 'Dear user,' . "\n\n" . 'You have not updated your website in %timetomail% days. This message is to remind you that your website will be deleted if it is not updated in %timetodeletion% days.' . "\n\n" . 'If you would like to keep your website, please update it before %date%.' . "\n\n" . 'Best regards.' . "\n\n";
$options['deletemessage']	= 'Dear user,' . "\n\n" . 'Since your website has not been updated for %timetodeletion% days, it has been deleted.' . "\n\n" . 'Please contact us if you have any questions.' . "\n\n" . 'Best regards.' . "\n\n";
 
$options['blogstoignore']	= array(); //blogs that should not be part of this logic for example =array(1,2);
 
/********************/
/*** STOP EDITING ***/
/********************/
 
$options['unix_timetomail']		= $options['timetomail'] * 24 * 60 * 60;
$options['unix_timetodeletion']	= $options['timetodeletion'] * 24 * 60 * 60;
$options['date']				= date($options['dateformat'], (($options['timetodeletion'] - $options['timetomail']) * 24 * 60 * 60) + time());
 
function mailer ($to, $from, $subject, $body){
	global $options;
	$headers = array ('From' => $from,
	  'To' => $to,
	  'Subject' => $subject);
	$smtp = Mail::factory('smtp',
		array ('host' => $options['email_host'],
		'auth' => false));
	$mail = $smtp->send($to, $headers, $body);
}
 
// Connect to database
$db =	mysql_connect($mysql_server,
					   $mysql_username,
					   $mysql_password);
 
		mysql_select_db($mysql_database, $db);
 
// Get blogs
$sql = 'SELECT blog_id,last_updated FROM wp_blogs';
$sql = mysql_query($sql);
 
		while( $row = mysql_fetch_assoc($sql) ) :
 
			if (!in_array($row['blog_id'], $options['blogstoignore']))
				$blogs_raw[$row['blog_id']] = strtotime($row['last_updated']);
 
		endwhile;
 
		// Get blogs for deletion notice
		foreach( $blogs_raw as $key => $value ) :
 
			$time = time() - $value;
 
			if( $time >= $options['unix_timetomail'] && $time < $options['unix_timetodeletion'] ) :
 
				$blogs_mail[] = array('blogid'  => $key,
									  'email'   => '',
									  'siteurl' => '');
 
			endif;
 
		endforeach;
 
				// Get userdata for warning notice
				for($i = 0; $i < sizeof($blogs_mail); $i++) :
 
					$str = 'admin_email';				
					$sql = mysql_query("SELECT option_value FROM wp_" . $blogs_mail[$i]['blogid'] . "_options WHERE option_name = '$str'");
 
						while ( $row = mysql_fetch_assoc($sql) ) :
 
							$blogs_mail[$i]['email'] = $row['option_value'];
 
						endwhile;
 
					$str = 'siteurl';
					$sql = mysql_query("SELECT option_value FROM wp_" . $blogs_mail[$i]['blogid'] . "_options WHERE option_name = '$str'");
 
						while ( $row = mysql_fetch_assoc($sql) ) :
 
							$blogs_mail[$i]['siteurl'] = $row['option_value'];
 
						endwhile;
 
				endfor;
 
						$cronmsg = 'The following blogs have been sent deletion warnings:' . "\n\n";
						// Create and send warning notice to users
						for($i = 0; $i < sizeof($blogs_mail); $i++) :
 
							$message = str_replace('%timetomail%', $options['timetomail'], $options['noticemessage']);
							$message = str_replace('%timetodeletion%', $options['timetodeletion'], $message);
							$message = str_replace('%date%', $options['date'], $message);
							//$message = str_replace('%loginurl%', $blogs_mail[$i]['siteurl'] . 'wp-login.php', $message);
 
							mailer($blogs_mail[$i]['email'], $options['email_from'], $options['email_subject'], $message);
 
							$cronmsg .= $blogs_mail[$i]['siteurl'] . "\n";
 
						endfor;
						if (sizeof($blogs_mail) > 0) mailer($options['mail_report'], $options['mail_report'], 'Blogs Sent Warnings', $cronmsg);
 
 
 
		// Get blogs for deletion
		foreach( $blogs_raw as $key => $value ) :
 
			$time = time() - $value;
 
			if( $time >= $options['unix_timetodeletion'] ) :
 
				$blogs_delete[] = array('blogid'  => $key,
										'email'   => '',
										'siteurl' => '');
 
			endif;
 
		endforeach;
 
				// Get userdata for deletion notice
				for($i = 0; $i < sizeof($blogs_delete); $i++) :
 
					$str = 'admin_email';				
					$sql = mysql_query("SELECT option_value FROM wp_" . $blogs_delete[$i]['blogid'] . "_options WHERE option_name = '$str'");
 
						while ( $row = mysql_fetch_assoc($sql) ) :
 
							$blogs_delete[$i]['email'] = $row['option_value'];
 
						endwhile;
 
					$str = 'siteurl';
					$sql = mysql_query("SELECT option_value FROM wp_" . $blogs_delete[$i]['blogid'] . "_options WHERE option_name = '$str'");
 
						while ( $row = mysql_fetch_assoc($sql) ) :
 
							$blogs_delete[$i]['siteurl'] = $row['option_value'];
 
						endwhile;
 
				endfor;
 
						// Delete tables
						$cronmsg = 'The following blogs have been deleted:' . "\n\n";
						for($i = 0; $i < sizeof($blogs_delete); $i++) :
 
							//loop though and delete all tables that begin with the blog id
							$prefix = 'wp_' . $blogs_delete[$i]['blogid'] . '_%';
 
							$querystr ="SHOW TABLES LIKE '$prefix'";
							$tables = $wpdb->get_results($querystr, ARRAY_N);
 
							$query = "";
							if ($tables){
								foreach ($tables as $table){
									$query = 'DROP TABLE IF EXISTS '. $table[0];
									$wpdb->query($query);
								}
							}
 
							$sql_blogid	= $blogs_delete[$i]['blogid'];
							mysql_query("DELETE FROM wp_blogs WHERE blog_id = '$sql_blogid'") or die('Error: ' . mysql_error());
 
							// Send mail
							$message = str_replace('%timetodeletion%', $options['timetodeletion'], $options['deletemessage']);
 
							mailer($blogs_delete[$i]['email'], $options['email_from'], $options['email_subject'], $message);
 
							$cronmsg .= $blogs_delete[$i]['siteurl'] . "\n";
 
						endfor;
						if (sizeof($blogs_delete) > 0) mailer($options['mail_report'], $options['mail_report'], 'Deleted Blogs', $cronmsg);
 
?>

It’s fairly simple to integrate Google Analytics so that your campaign variables get saved behind the scenes when someone submits your wufoo form.

The code is based on the original post at analytics talk, with some small modifications for wufoo.

All we need to do is include a small javascript file that will parse our google analytics cookie and pass it to wufoo using url modification.

Here’s a 4 minute screencast of what we’ll be achieving:

Note that the code relies on the old version of the google analytics code (urchin.js update: you can find updated javascript for the new version of the tracking code at Analytics Talk, however I have not yet integrated it with wufoo) and I’m using jquery for $(document).ready functionality, but you could use your library of choice if you don’t use jQuery.

The complete javascript is

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
$(document).ready(function(){
    // 
    // Get the __utmz cookie value. This is the cookies that 
    // stores all campaign information. 
    // 
    var z = _uGC(document.cookie, '__utmz=', ';'); 
    // 
    // The cookie has a number of name-value pairs. 
    // Each identifies an aspect of the campaign. 
    // 
    // utmcsr  = campaign source 
    // utmcmd  = campaign medium 
    // utmctr  = campaign term (keyword) 
    // utmcct  = campaign content (used for A/B testing) 
    // utmccn  = campaign name 
    // utmgclid = unique identifier used when AdWords auto tagging is enabled 
    // 
    // This is very basic code. It separates the campaign-tracking cookie 
    // and populates a variable with each piece of campaign info. 
    // 
    var source  = _uGC(z, 'utmcsr=', '|'); 
    var medium  = _uGC(z, 'utmcmd=', '|'); 
    var term    = _uGC(z, 'utmctr=', '|'); 
    var content = _uGC(z, 'utmcct=', '|'); 
    var campaign = _uGC(z, 'utmccn=', '|'); 
    var gclid   = _uGC(z, 'utmgclid=', '|'); 
    // 
    // The gclid is ONLY present when auto tagging has been enabled. 
    // All other variables, except the term variable, will be '(not set)'. 
    // Because the gclid is only present for Google AdWords we can 
    // populate some other variables that would normally 
    // be left blank. 
    // 
    if (gclid !="-") { 
          source = 'google'; 
          medium = 'cpc'; 
    } 
    // Data from the custom segmentation cookie can also be passed 
    // back to wufoo via url modification
    var csegment = _uGC(document.cookie, '__utmv=', ';'); 
    if (csegment != '-') { 
          var csegmentex = /[1-9]*?\.(.*)/;
          csegment    = csegment.match(csegmentex); 
          csegment    = csegment[1]; 
    } else { 
          csegment = ''; 
    } 
    //modify the wufoo iframe location to pass the analytics values
    frames[0].location.href = 'https://youraccount.wufoo.com/embed/123/def/field30=' + source + '&field36=' + medium + '&field35=' + term + '&field34=' + content + '&field33=' + campaign + '&field32=' + gclid;
 
});

Current versions of Umbraco appear to have a bug that may cause corruption of publishing times and cause documents to re-publish themselves many times over. This can lead to an extremely large CMSPropertyData table and the need for frequent application pool recycles. See http://forum.umbraco.org/yaf_postst4699p2_Extremely-large-Umbraco-database-55GB-table-cmsPropertyData-contains-over-135-million-records.aspx and http://forum.umbraco.org/yaf_postst6564_305-Database-Huge–cmsPropertyData–392MB.aspx for a discussion of the issue.

We’ve done several things to control this issue, though a true fix has yet to be developed:

  1. We applied the suggested fix of changing the umbraco.presentation.cache.CacheRefresher() constructor (this seemed to have no effect):
    public CacheRefresher() {
       if (System.Web.HttpContext.Current != null)
        {
            this.Url = "http://" + System.Web.HttpContext.Current.Request.ServerVariables["SERVER_NAME"] + "/umbraco/webservices/cacheRefresher.asmx";
        }
    }
  2. We removed the future publishing functionality from the UI (umbraco/presentation/umbraco/editContent.aspx.cs in umbraco 3.03):
    //dpRelease.ID = "releaseDate";
    //dpRelease.DateTime = _document.ReleaseDate;
    //dpRelease.ShowTime = true;
    //publishProps.addProperty(ui.Text("content", "releaseDate", base.getUser()), dpRelease);
    //dpExpire.ID = "expireDate";
    //dpExpire.DateTime = _document.ExpireDate;
    //dpExpire.ShowTime = true;
    //publishProps.addProperty(ui.Text("content", "expireDate", base.getUser()), dpExpire);
  3. We commended out the publishing timer which seems to have stopped the growth without adverse effects (thought I imagine that scheduled publishing would no longer work) (/umbraco/presentation/requestModule.cs in umbraco 3.03)
    //publishingTimer =
    //    new Timer(new TimerCallback(publishingService.CheckPublishing), HttpApp.Context, 600000, 60000);

The author of ClientTools promises to release a new version which should be able to cleanup the large number of item revisions in the database.

Stored procedures useful for troubleshooting this issue include:

  • find items with the most revisions:
    SELECT contentNodeId, count(contentNodeId) FROM cmsPropertyData GROUP BY contentNodeId ORDER BY count(contentNodeid) DESC
  • manually delete an item (if you can’t find the item in the UI using http://site/umbraco/editContent.aspx?id=IDHERE):
    DELETE FROM cmsPropertyData WHERE contentNodeId = '4023'
  • find lost documents:
    SELECT * FROM umbracoNode
    WHERE nodeObjectType = 'c66ba18e-eaf3-4cff-8a22-41b16d66a972' AND ID NOT IN (SELECT Nodeid FROM cmsDocument)
  • find items scheduled to publish:
    SELECT DISTINCT nodeId, level, sortOrder FROM cmsDocument INNER JOIN umbracoNode ON umbracoNode.id = cmsDocument.nodeId WHERE newest = 1 AND NOT releaseDate IS NULL AND releaseDate < = getdate() ORDER BY level, sortOrder

I’m working on a Wordpress MU blog network and ran accross the need to change the domain that the blogs are running under. This should actually be a fairly common need, for example, if you copy your PROD environment to QA, you’ll probably want to change the blog url from http://blog.com to http://qa.blog.com.

Changing the domain name starts out pretty straightforward:

  • edit the “domain” in the wp_site table.
  • edit all “domain” entries in the wp_blogs table: UPDATE wp_blogs SET domain = REPLACE(domain,”blog.com”,”qa.blog.com”)

But then, you’ll notice that there’s a lot of hard-coded urls in the wp_#_options table, which exists for every blog in your network.

So, here’s a little PHP file that you can put in the root of your Wordpress MU install (or elsewhere). This will loop though these tables and do the replaces you need:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
< ?php
require_once('wp-load.php');
 
$querystr ="SHOW TABLES LIKE 'wp_%_options'";
$tables = $wpdb->get_results($querystr, ARRAY_N);
echo count($tables);
$query = "";
if ($tables){
	foreach ($tables as $table){
		$query = 'UPDATE '.$table[0].' SET option_value = REPLACE(option_value,"blog.com","qa.blog.com")';
		$wpdb->query($query);
	}
}
?>

The file will output a count of the number of tables that were affected. Ta da